mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-20 04:09:16 +00:00
Compare commits
36 Commits
5346
...
new-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63754f01a5 | ||
|
|
dcf677bdf0 | ||
|
|
eaea2cdef0 | ||
|
|
bcad87e894 | ||
|
|
d500493649 | ||
|
|
8ad3ef0022 | ||
|
|
d0664d9ea0 | ||
|
|
45c40d7afc | ||
|
|
e33edfd2a5 | ||
|
|
96b2a07172 | ||
|
|
0e8c01af75 | ||
|
|
3049ad6dbf | ||
|
|
9a30ad87a7 | ||
|
|
11c05560c4 | ||
|
|
d92fa59c74 | ||
|
|
4eb1d16faa | ||
|
|
91adc70724 | ||
|
|
15b083c182 | ||
|
|
26fdaf7b8d | ||
|
|
64a9874d63 | ||
|
|
753cffc458 | ||
|
|
99e56e229d | ||
|
|
c3dae1f6e9 | ||
|
|
703e43ecd7 | ||
|
|
0bf52b613b | ||
|
|
584ec7c82e | ||
|
|
5f5cac0e01 | ||
|
|
0a9b9bb41d | ||
|
|
5ad98d193a | ||
|
|
4cd5be9d8b | ||
|
|
5514be630d | ||
|
|
6a6146727f | ||
|
|
52e9e90b3a | ||
|
|
7ff3b669ee | ||
|
|
e791c4f70c | ||
|
|
fc75fd9644 |
@@ -78,7 +78,7 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
||||
|
||||
if (!rootProject.ext.libreBuild) {
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
@@ -109,6 +109,7 @@ gradle.projectsEvaluated {
|
||||
def dropboxActivity = """
|
||||
<activity
|
||||
android:configChanges="keyboard|orientation"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:name="com.dropbox.core.android.AuthActivity">
|
||||
<intent-filter>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
android:installLocation="auto">
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
@@ -13,6 +14,7 @@
|
||||
android:resource="@xml/app_restrictions" />
|
||||
<activity
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -18,9 +18,9 @@ buildscript {
|
||||
|
||||
ext {
|
||||
buildToolsVersion = "30.0.3"
|
||||
compileSdkVersion = 30
|
||||
compileSdkVersion = 31
|
||||
minSdkVersion = 23
|
||||
targetSdkVersion = 30
|
||||
targetSdkVersion = 31
|
||||
supportLibVersion = "28.0.0"
|
||||
|
||||
// The Maven artifact groupdId of the third-party react-native modules which
|
||||
|
||||
@@ -24,6 +24,7 @@ android.enableDexingArtifactTransform.desugaring=false
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=21.4.0
|
||||
sdkVersion=3.9.1
|
||||
appVersion=21.4.1
|
||||
sdkVersion=3.10.2
|
||||
|
||||
@@ -82,7 +82,7 @@ class OngoingNotification {
|
||||
}
|
||||
|
||||
Intent notificationIntent = new Intent(context, context.getClass());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ONGOING_CONFERENCE_CHANNEL_ID);
|
||||
|
||||
@@ -125,7 +125,7 @@ class OngoingNotification {
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
intent.setAction(action.getName());
|
||||
PendingIntent pendingIntent
|
||||
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
String title = context.getString(titleId);
|
||||
return new NotificationCompat.Action(0, title, pendingIntent);
|
||||
}
|
||||
|
||||
30
config.js
30
config.js
@@ -74,8 +74,11 @@ var config = {
|
||||
// callStatsThreshold: 5 // enable callstats for 5% of the users.
|
||||
},
|
||||
|
||||
// Enables reactions feature.
|
||||
// enableReactions: false,
|
||||
// Disables moderator indicators.
|
||||
// disableModeratorIndicator: false,
|
||||
|
||||
// Disables the reactions feature.
|
||||
// disableReactions: true,
|
||||
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
@@ -904,15 +907,32 @@ var config = {
|
||||
// If true, tile view will not be enabled automatically when the participants count threshold is reached.
|
||||
// disableTileView: true,
|
||||
|
||||
// Controls the visibility and behavior of the top header conference info labels.
|
||||
// If a label's id is not in any of the 2 arrays, it will not be visible at all on the header.
|
||||
// conferenceInfo: {
|
||||
// // those labels will not be hidden in tandem with the toolbox.
|
||||
// alwaysVisible: ['recording', 'local-recording'],
|
||||
// // those labels will be auto-hidden in tandem with the toolbox buttons.
|
||||
// autoHide: [
|
||||
// 'subject',
|
||||
// 'conference-timer',
|
||||
// 'participants-count',
|
||||
// 'e2ee',
|
||||
// 'transcribing',
|
||||
// 'video-quality',
|
||||
// 'insecure-room'
|
||||
// ]
|
||||
// },
|
||||
|
||||
// Hides the conference subject
|
||||
// hideConferenceSubject: true,
|
||||
|
||||
// Hides the recording label
|
||||
// hideRecordingLabel: false,
|
||||
|
||||
// Hides the conference timer.
|
||||
// hideConferenceTimer: true,
|
||||
|
||||
// Hides the recording label
|
||||
// hideRecordingLabel: false,
|
||||
|
||||
// Hides the participants stats
|
||||
// hideParticipantsStats: true,
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.reactions-menu {
|
||||
width: 280px;
|
||||
background: #292929;
|
||||
background: $menuBG;
|
||||
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
|
||||
@@ -1,54 +1,22 @@
|
||||
.subject {
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
margin-top: 20px;
|
||||
position: absolute;
|
||||
top: -120px;
|
||||
transition: top .3s ease-in;
|
||||
width: 100%;
|
||||
margin-top: -120px;
|
||||
transition: margin-top .3s ease-in;
|
||||
z-index: $zindex3;
|
||||
|
||||
&.visible {
|
||||
top: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&.recording {
|
||||
top: 0;
|
||||
|
||||
.subject-details-container {
|
||||
opacity: 0;
|
||||
transition: opacity .3s ease-in;
|
||||
}
|
||||
|
||||
.subject-info-container .show-always {
|
||||
transition: margin-left .3s ease-in;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
.subject-details-container {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subject-details-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.subject-info-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: calc(100% - 280px);
|
||||
margin: 0 auto;
|
||||
|
||||
&--full-width {
|
||||
max-width: 100%;
|
||||
}
|
||||
height: 28px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,21 +31,47 @@
|
||||
.subject-text {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 3px 0px 0px 3px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 2px 16px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding: 0 16px;
|
||||
height: 28px;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 300px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.subject-timer {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 0px 3px 3px 0px;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
line-height: 28px;
|
||||
min-width: 34px;
|
||||
padding: 6px 8px;
|
||||
padding: 0 8px;
|
||||
height: 28px;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.details-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
margin-bottom: 3px;
|
||||
margin-left: $remoteVideoMenuIconMargin;
|
||||
}
|
||||
|
||||
.self-view-mobile-portrait video {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,3 +116,11 @@
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides for self view when in portrait mode on mobile.
|
||||
* This is done in order to keep the aspect ratio.
|
||||
*/
|
||||
.vertical-filmstrip .self-view-mobile-portrait #localVideo_container {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ var interfaceConfig = {
|
||||
|
||||
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
|
||||
|
||||
DISABLE_FOCUS_INDICATOR: false,
|
||||
// Deprecated. Please use disableModeratorIndicator from config.js
|
||||
// DISABLE_FOCUS_INDICATOR: false,
|
||||
|
||||
/**
|
||||
* If true, notifications regarding joining/leaving are no longer displayed.
|
||||
|
||||
@@ -299,7 +299,7 @@ PODS:
|
||||
- react-native-video/Video (= 5.1.1)
|
||||
- react-native-video/Video (5.1.1):
|
||||
- React-Core
|
||||
- react-native-webrtc (1.92.1):
|
||||
- react-native-webrtc (1.92.2):
|
||||
- React-Core
|
||||
- react-native-webview (11.0.2):
|
||||
- React-Core
|
||||
@@ -584,7 +584,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
|
||||
react-native-webrtc: 77b969fe6bc5b7c93b455f93a13698812fb8bb4e
|
||||
react-native-webrtc: 41526e4060dac373c18676f866962d4180ee47b9
|
||||
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.4.0</string>
|
||||
<string>21.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.4.0</string>
|
||||
<string>21.4.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.4.0</string>
|
||||
<string>21.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.4.0</string>
|
||||
<string>21.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.9.0</string>
|
||||
<string>3.10.4</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
}
|
||||
|
||||
- (void)destroyReactNativeBridge {
|
||||
[_bridgeWrapper invalidate];
|
||||
_bridgeWrapper = nil;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,4 +34,6 @@
|
||||
|
||||
@property (nonatomic, readonly, strong) RCTBridge *bridge;
|
||||
|
||||
- (void)invalidate;
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)invalidate {
|
||||
[_bridge invalidate];
|
||||
}
|
||||
|
||||
#pragma mark helper methods for getting the packager URL
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"youtubeTerms": "Условия использования YouTube"
|
||||
},
|
||||
"lobby": {
|
||||
"allow": "Разрешить",
|
||||
"disableDialogContent": "В настоящее время включен режим лобби. Эта функция гарантирует, что нежелательные участники не смогут присоединиться к вашей встрече. Вы хотите его отключить?",
|
||||
"disableDialogSubmit": "Отключить",
|
||||
"emailField": "Введите ваш адрес электронной почты",
|
||||
@@ -750,6 +751,7 @@
|
||||
"moreOptions": "Меню доп. настроек",
|
||||
"mute": "Вкл/Выкл звук",
|
||||
"muteEveryone": "Выкл. микрофон у всех",
|
||||
"participants": "Участники",
|
||||
"pip": "Вкл/Выкл режим Картинка-в-картинке",
|
||||
"privateMessage": "Отправить личное сообщение",
|
||||
"profile": "Редактировать профиль",
|
||||
@@ -807,6 +809,7 @@
|
||||
"noisyAudioInputDesc": "Возможно, ваш микрофон создает шум. Вы можете выключить его или смените устройство.",
|
||||
"noisyAudioInputTitle": "Похоже, ваш микрофон создает шум!",
|
||||
"openChat": "Открыть чат",
|
||||
"participants": "Участники",
|
||||
"pip": "Вкл режим Картинка-в-картинке",
|
||||
"privateMessage": "Отправить личное сообщение",
|
||||
"profile": "Редактировать профиль",
|
||||
|
||||
@@ -213,7 +213,9 @@
|
||||
"done": "Done",
|
||||
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
|
||||
"e2eeLabel": "Enable End-to-End Encryption",
|
||||
"e2eeDisabledDueToMaxModeDescription": "Cannot enable End-to-End Encryption due to large number of participants in the conference.",
|
||||
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
|
||||
"e2eeWillDisableDueToMaxModeDescription": "WARNING: End-to-End Encryption will be automatically disabled if more participants join the conference.",
|
||||
"enterDisplayName": "Enter your name here",
|
||||
"embedMeeting": "Embed meeting",
|
||||
"error": "Error",
|
||||
@@ -568,7 +570,7 @@
|
||||
"mutedTitle": "You're muted!",
|
||||
"mutedRemotelyTitle": "You've been muted by {{moderator}}",
|
||||
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
|
||||
"videoMutedRemotelyTitle": "Your camera has been turned off by {{moderator}}",
|
||||
"videoMutedRemotelyTitle": "Your video has been turned off by {{moderator}}",
|
||||
"videoMutedRemotelyDescription": "You can always turn it on again.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
|
||||
@@ -613,6 +615,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Allow attendees to:",
|
||||
"allowVideo": "Allow video",
|
||||
"audioModeration": "Unmute themselves",
|
||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||
"invite": "Invite Someone",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../../react/features/base/conference';
|
||||
import { overwriteConfig, getWhitelistedJSON } from '../../react/features/base/config';
|
||||
import { toggleDialog } from '../../react/features/base/dialog/actions';
|
||||
import { isSupportedBrowser } from '../../react/features/base/environment';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../react/features/base/media';
|
||||
@@ -692,6 +693,7 @@ class API {
|
||||
this._enabled = true;
|
||||
|
||||
initCommands();
|
||||
this.notifyBrowserSupport(isSupportedBrowser());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1355,6 +1357,19 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) wether the used browser is supported or not.
|
||||
*
|
||||
* @param {boolean} supported - If browser is supported or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyBrowserSupport(supported: boolean) {
|
||||
this._sendEvent({
|
||||
name: 'browser-support',
|
||||
supported
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
|
||||
1
modules/API/external/external_api.js
vendored
1
modules/API/external/external_api.js
vendored
@@ -76,6 +76,7 @@ const events = {
|
||||
'avatar-changed': 'avatarChanged',
|
||||
'audio-availability-changed': 'audioAvailabilityChanged',
|
||||
'audio-mute-status-changed': 'audioMuteStatusChanged',
|
||||
'browser-support': 'browserSupport',
|
||||
'camera-error': 'cameraError',
|
||||
'chat-updated': 'chatUpdated',
|
||||
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -2752,8 +2752,8 @@
|
||||
}
|
||||
},
|
||||
"@jitsi/sdp-interop": {
|
||||
"version": "github:jitsi/sdp-interop#98cd62cc00f92c8c2430e52ca746a86813658e83",
|
||||
"from": "github:jitsi/sdp-interop#98cd62cc00f92c8c2430e52ca746a86813658e83",
|
||||
"version": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",
|
||||
"from": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"sdp-transform": "2.14.1"
|
||||
@@ -11117,11 +11117,11 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
|
||||
"from": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
|
||||
"version": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
|
||||
"from": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#98cd62cc00f92c8c2430e52ca746a86813658e83",
|
||||
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",
|
||||
"@jitsi/sdp-simulcast": "0.4.0",
|
||||
"async": "0.9.0",
|
||||
"base64-js": "1.3.1",
|
||||
@@ -15201,9 +15201,8 @@
|
||||
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "1.92.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.92.1.tgz",
|
||||
"integrity": "sha512-cbFU1acL9aud/ohk/40jwahSNac4PwBKdmRPgXb9WqaGwxAr4CgSsrBRmruCISMdw0Pec+ZoZtYjXVuHzF51sg==",
|
||||
"version": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
|
||||
"from": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"cross-os": "^1.3.0",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -92,7 +92,7 @@
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-video": "5.1.1",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "1.92.1",
|
||||
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
|
||||
"react-native-webview": "11.0.2",
|
||||
"react-native-youtube-iframe": "2.1.1",
|
||||
"react-redux": "7.1.0",
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
diff --git a/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m b/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
|
||||
index d9387c4..a487da0 100644
|
||||
--- a/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
|
||||
+++ b/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
|
||||
@@ -165,10 +165,10 @@ - (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
|
||||
NSNumber *socketID = [webSocket reactTag];
|
||||
_contentHandlers[socketID] = nil;
|
||||
_sockets[socketID] = nil;
|
||||
- [self sendEventWithName:@"websocketFailed" body:@{
|
||||
- @"message": error.localizedDescription,
|
||||
- @"id": socketID
|
||||
- }];
|
||||
+ NSDictionary *body =
|
||||
+ @{@"message" : error.localizedDescription ?: @"Undefined, error is nil",
|
||||
+ @"id" : socketID ?: @(-1)};
|
||||
+ [self sendEventWithName:@"websocketFailed" body:body];
|
||||
}
|
||||
|
||||
- (void)webSocket:(RCTSRWebSocket *)webSocket
|
||||
diff --git a/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm b/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm
|
||||
index bd48f44..d243ed0 100644
|
||||
--- a/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import '../analytics/middleware';
|
||||
import '../av-moderation/middleware';
|
||||
import '../base/conference/middleware';
|
||||
import '../base/config/middleware';
|
||||
import '../base/jwt/middleware';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import '../authentication/middleware';
|
||||
import '../av-moderation/middleware';
|
||||
import '../base/devices/middleware';
|
||||
import '../e2ee/middleware';
|
||||
import '../external-api/middleware';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import '../analytics/reducer';
|
||||
import '../authentication/reducer';
|
||||
import '../av-moderation/reducer';
|
||||
import '../base/app/reducer';
|
||||
import '../base/audio-only/reducer';
|
||||
import '../base/color-scheme/reducer';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import '../av-moderation/reducer';
|
||||
import '../base/devices/reducer';
|
||||
import '../e2ee/reducer';
|
||||
import '../feedback/reducer';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { getConferenceState } from '../base/conference';
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import { getParticipantById } from '../base/participants';
|
||||
import { isForceMuted } from '../participants-pane/functions';
|
||||
|
||||
import {
|
||||
DISMISS_PENDING_PARTICIPANT,
|
||||
@@ -27,11 +29,15 @@ import { isEnabledFromState } from './functions';
|
||||
export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { conference } = getConferenceState(state);
|
||||
const participant = getParticipantById(state, id);
|
||||
|
||||
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state)) {
|
||||
const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
|
||||
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||
|
||||
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state) && isAudioForceMuted) {
|
||||
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
|
||||
}
|
||||
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state)) {
|
||||
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) {
|
||||
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,8 +135,11 @@ function _addConferenceListeners(conference, dispatch, state) {
|
||||
|
||||
// Remove the tracks from peerconnection as well.
|
||||
for (const track of localTracks) {
|
||||
if ((audioMuted && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)
|
||||
|| (videoMuted && track.jitsiTrack.getType() === MEDIA_TYPE.VIDEO)) {
|
||||
const trackType = track.jitsiTrack.getType();
|
||||
|
||||
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
|
||||
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|
||||
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
|
||||
dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export default [
|
||||
*/
|
||||
'callUUID',
|
||||
|
||||
'conferenceInfo',
|
||||
'channelLastN',
|
||||
'connectionIndicators',
|
||||
'constraints',
|
||||
@@ -95,9 +96,11 @@ export default [
|
||||
'disableIncomingMessageSound',
|
||||
'disableJoinLeaveSounds',
|
||||
'disableLocalVideoFlip',
|
||||
'disableModeratorIndicator',
|
||||
'disableNS',
|
||||
'disablePolls',
|
||||
'disableProfile',
|
||||
'disableReactions',
|
||||
'disableRecordAudioNotification',
|
||||
'disableRemoteControl',
|
||||
'disableRemoteMute',
|
||||
@@ -125,7 +128,6 @@ export default [
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
'enableOpusRed',
|
||||
'enableReactions',
|
||||
'enableRemb',
|
||||
'enableSaveLogs',
|
||||
'enableScreenshotCapture',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { CONFERENCE_INFO } from '../../conference/components/constants';
|
||||
import { equals, ReducerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
@@ -56,6 +57,17 @@ const INITIAL_RN_STATE = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping between old configs controlling the conference info headers visibility and the
|
||||
* new configs. Needed in order to keep backwards compatibility.
|
||||
*/
|
||||
const CONFERENCE_HEADER_MAPPING = {
|
||||
hideConferenceTimer: [ 'conference-timer' ],
|
||||
hideConferenceSubject: [ 'subject' ],
|
||||
hideParticipantsStats: [ 'participants-count' ],
|
||||
hideRecordingLabel: [ 'recording', 'local-recording' ]
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_CONFIG:
|
||||
@@ -172,6 +184,27 @@ function _setConfig(state, { config }) {
|
||||
return equals(state, newState) ? state : newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the conferenceInfo object against the defaults.
|
||||
*
|
||||
* @param {Object} config - The old config.
|
||||
* @returns {Object} The processed conferenceInfo object.
|
||||
*/
|
||||
function _getConferenceInfo(config) {
|
||||
const { conferenceInfo } = config;
|
||||
|
||||
if (conferenceInfo) {
|
||||
return {
|
||||
alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
|
||||
autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...CONFERENCE_INFO
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new config {@code Object}, if necessary, out of a specific
|
||||
* config {@code Object} which is in the latest format supported by jitsi-meet.
|
||||
@@ -194,6 +227,27 @@ function _translateLegacyConfig(oldValue: Object) {
|
||||
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
|
||||
}
|
||||
|
||||
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]);
|
||||
|
||||
if (filteredConferenceInfo.length) {
|
||||
newValue.conferenceInfo = _getConferenceInfo(oldValue);
|
||||
|
||||
filteredConferenceInfo.forEach(key => {
|
||||
// hideRecordingLabel does not mean not render it at all, but autoHide it
|
||||
if (key === 'hideRecordingLabel') {
|
||||
newValue.conferenceInfo.alwaysVisible
|
||||
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
|
||||
newValue.conferenceInfo.autoHide
|
||||
= _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
|
||||
} else {
|
||||
newValue.conferenceInfo.alwaysVisible
|
||||
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
|
||||
newValue.conferenceInfo.autoHide
|
||||
= newValue.conferenceInfo.autoHide.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!oldValue.connectionIndicators
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
|
||||
@@ -232,6 +286,12 @@ function _translateLegacyConfig(oldValue: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.disableModeratorIndicator === undefined
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
|
||||
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||
renderHeader
|
||||
? _styles.sheetHeader
|
||||
: _styles.sheet,
|
||||
renderFooter && _styles.sheetFooter,
|
||||
style,
|
||||
{
|
||||
maxHeight: _height - 100
|
||||
@@ -154,7 +155,10 @@ class BottomSheet extends PureComponent<Props> {
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { addScrollViewPadding && styles.scrollView } >
|
||||
style = { [
|
||||
renderFooter && _styles.sheet,
|
||||
addScrollViewPadding && styles.scrollView
|
||||
] } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
{ renderFooter && renderFooter() }
|
||||
|
||||
@@ -213,6 +213,13 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
*/
|
||||
sheetHeader: {
|
||||
backgroundColor: BaseTheme.palette.ui02
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's background color with footer.
|
||||
*/
|
||||
sheetFooter: {
|
||||
backgroundColor: BaseTheme.palette.bottomSheet
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { batch } from 'react-redux';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { approveParticipant } from '../../av-moderation/actions';
|
||||
import { toggleE2EE } from '../../e2ee/actions';
|
||||
import { MAX_MODE } from '../../e2ee/constants';
|
||||
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
||||
import { isForceMuted } from '../../participants-pane/functions';
|
||||
import { CALLING, INVITED } from '../../presence-status';
|
||||
@@ -327,16 +328,20 @@ StateListenerRegistry.register(
|
||||
/**
|
||||
* Handles a E2EE enabled status update.
|
||||
*
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} conference - The conference for which we got an update.
|
||||
* @param {string} participantId - The ID of the participant from which we got an update.
|
||||
* @param {boolean} newValue - The new value of the E2EE enabled status.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _e2eeUpdated({ dispatch }, conference, participantId, newValue) {
|
||||
function _e2eeUpdated({ getState, dispatch }, conference, participantId, newValue) {
|
||||
const e2eeEnabled = newValue === 'true';
|
||||
|
||||
dispatch(toggleE2EE(e2eeEnabled));
|
||||
const { maxMode } = getState()['features/e2ee'] || {};
|
||||
|
||||
if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) {
|
||||
dispatch(toggleE2EE(e2eeEnabled));
|
||||
}
|
||||
|
||||
dispatch(participantUpdated({
|
||||
conference,
|
||||
@@ -541,6 +546,7 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
|
||||
title: getParticipantDisplayName(state, participantId),
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
raiseHandNotification: true,
|
||||
concatText: true,
|
||||
...action
|
||||
}, NOTIFICATION_TIMEOUT * (shouldDisplayAllowAction ? 2 : 1)));
|
||||
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
||||
|
||||
@@ -55,9 +55,12 @@ export function createDesiredLocalTracks(...desiredTypes) {
|
||||
dispatch(destroyLocalDesktopTrackIfExists());
|
||||
|
||||
if (desiredTypes.length === 0) {
|
||||
const { audio, video } = state['features/base/media'];
|
||||
const { video } = state['features/base/media'];
|
||||
|
||||
audio.muted || desiredTypes.push(MEDIA_TYPE.AUDIO);
|
||||
// XXX: Always create the audio track early, even if it will be muted.
|
||||
// This fixes a timing issue when adding the track to the conference which
|
||||
// manifests primarily on iOS 15.
|
||||
desiredTypes.push(MEDIA_TYPE.AUDIO);
|
||||
|
||||
// XXX When the app is coming into the foreground from the
|
||||
// background in order to handle a URL, it may realize the new
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
* "error" or "local" or "remote".
|
||||
* @param {string} messageDetails.timestamp - A timestamp to display for when
|
||||
* the message was received.
|
||||
* @param {string} messageDetails.isReaction - Whether or not the
|
||||
* message is a reaction message.
|
||||
* @returns {{
|
||||
* type: ADD_MESSAGE,
|
||||
* displayName: string,
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
* message: string,
|
||||
* messageType: string,
|
||||
* timestamp: string,
|
||||
* isReaction: boolean
|
||||
* }}
|
||||
*/
|
||||
export function addMessage(messageDetails: Object) {
|
||||
|
||||
@@ -69,14 +69,30 @@ export function getUnreadCount(state: Object) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let reactionMessages = 0;
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
// React native stores the messages in a reversed order.
|
||||
return messages.indexOf(lastReadMessage);
|
||||
const lastReadIndex = messages.indexOf(lastReadMessage);
|
||||
|
||||
for (let i = 0; i < lastReadIndex; i++) {
|
||||
if (messages[i].isReaction) {
|
||||
reactionMessages++;
|
||||
}
|
||||
}
|
||||
|
||||
return lastReadIndex - reactionMessages;
|
||||
}
|
||||
|
||||
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
|
||||
|
||||
return messagesCount - (lastReadIndex + 1);
|
||||
for (let i = lastReadIndex + 1; i < messagesCount; i++) {
|
||||
if (messages[i].isReaction) {
|
||||
reactionMessages++;
|
||||
}
|
||||
}
|
||||
|
||||
return messagesCount - (lastReadIndex + 1) - reactionMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case ADD_MESSAGE:
|
||||
unreadCount = action.hasRead ? 0 : getUnreadCount(getState()) + 1;
|
||||
unreadCount = getUnreadCount(getState());
|
||||
if (action.isReaction) {
|
||||
action.hasRead = false;
|
||||
} else {
|
||||
unreadCount = action.hasRead ? 0 : unreadCount + 1;
|
||||
}
|
||||
isOpen = getState()['features/chat'].isOpen;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
@@ -171,7 +176,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
message: action.message,
|
||||
privateMessage: false,
|
||||
timestamp: Date.now()
|
||||
}, false);
|
||||
}, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +275,7 @@ function _addChatMsgListener(conference, store) {
|
||||
message: getReactionMessageFromBuffer(eventData.reactions),
|
||||
privateMessage: false,
|
||||
timestamp: eventData.timestamp
|
||||
}, false);
|
||||
}, false, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -304,11 +309,13 @@ function _handleChatError({ dispatch }, error) {
|
||||
* @param {Store} store - The Redux store.
|
||||
* @param {Object} message - The message object.
|
||||
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
|
||||
* @param {boolean} isReaction - Whether or not the message is a reaction message.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState },
|
||||
{ id, message, privateMessage, timestamp },
|
||||
shouldPlaySound = true
|
||||
shouldPlaySound = true,
|
||||
isReaction = false
|
||||
) {
|
||||
// Logic for all platforms:
|
||||
const state = getState();
|
||||
@@ -337,7 +344,8 @@ function _handleReceivedMessage({ dispatch, getState },
|
||||
message,
|
||||
privateMessage,
|
||||
recipient: getParticipantDisplayName(state, localParticipant.id),
|
||||
timestamp: millisecondsTimestamp
|
||||
timestamp: millisecondsTimestamp,
|
||||
isReaction
|
||||
}));
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
|
||||
@@ -28,6 +28,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
id: action.id,
|
||||
isReaction: action.isReaction,
|
||||
messageType: action.messageType,
|
||||
message: action.message,
|
||||
privateMessage: action.privateMessage,
|
||||
|
||||
12
react/features/conference/components/constants.js
Normal file
12
react/features/conference/components/constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const CONFERENCE_INFO = {
|
||||
alwaysVisible: [ 'recording', 'local-recording' ],
|
||||
autoHide: [
|
||||
'subject',
|
||||
'conference-timer',
|
||||
'participants-count',
|
||||
'e2ee',
|
||||
'transcribing',
|
||||
'video-quality',
|
||||
'insecure-room'
|
||||
]
|
||||
};
|
||||
22
react/features/conference/components/functions.js
Normal file
22
react/features/conference/components/functions.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import { CONFERENCE_INFO } from './constants';
|
||||
|
||||
/**
|
||||
* Retrieves the conference info labels based on config values and defaults.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object} The conferenceInfo object.
|
||||
*/
|
||||
export const getConferenceInfo = (state: Object) => {
|
||||
const { conferenceInfo } = state['features/base/config'];
|
||||
|
||||
if (conferenceInfo) {
|
||||
return {
|
||||
alwaysVisible: conferenceInfo.alwaysVisible ?? CONFERENCE_INFO.alwaysVisible,
|
||||
autoHide: conferenceInfo.autoHide ?? CONFERENCE_INFO.autoHide
|
||||
};
|
||||
}
|
||||
|
||||
return CONFERENCE_INFO;
|
||||
};
|
||||
@@ -1,22 +1,22 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { E2EELabel } from '../../../e2ee';
|
||||
import { LocalRecordingLabel } from '../../../local-recording';
|
||||
import { getSessionStatusToShow, RecordingLabel } from '../../../recording';
|
||||
import { RecordingLabel } from '../../../recording';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { TranscribingLabel } from '../../../transcribing';
|
||||
import { VideoQualityLabel } from '../../../video-quality';
|
||||
import ConferenceTimer from '../ConferenceTimer';
|
||||
import { getConferenceInfo } from '../functions';
|
||||
|
||||
import ConferenceInfoContainer from './ConferenceInfoContainer';
|
||||
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
|
||||
import ParticipantsCount from './ParticipantsCount';
|
||||
|
||||
import { InsecureRoomNameLabel } from '.';
|
||||
import SubjectText from './SubjectText';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Subject}.
|
||||
@@ -24,54 +24,59 @@ import { InsecureRoomNameLabel } from '.';
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether the info should span across the full width.
|
||||
* The conference info labels to be shown in the conference header.
|
||||
*/
|
||||
_fullWidth: boolean,
|
||||
|
||||
/**
|
||||
* Whether the conference name and timer should be displayed or not.
|
||||
*/
|
||||
_hideConferenceNameAndTimer: boolean,
|
||||
|
||||
/**
|
||||
* Whether the conference timer should be shown or not.
|
||||
*/
|
||||
_hideConferenceTimer: boolean,
|
||||
|
||||
/**
|
||||
* Whether the recording label should be shown or not.
|
||||
*/
|
||||
_hideRecordingLabel: boolean,
|
||||
|
||||
/**
|
||||
* Whether the participant count should be shown or not.
|
||||
*/
|
||||
_showParticipantCount: boolean,
|
||||
|
||||
/**
|
||||
* The subject or the of the conference.
|
||||
* Falls back to conference name.
|
||||
*/
|
||||
_subject: string,
|
||||
_conferenceInfo: Object,
|
||||
|
||||
/**
|
||||
* Indicates whether the component should be visible or not.
|
||||
*/
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the recording label is visible.
|
||||
*/
|
||||
_recordingLabel: boolean
|
||||
_visible: boolean
|
||||
};
|
||||
|
||||
const getLeftMargin = () => {
|
||||
const subjectContainerWidth = document.getElementById('subject-container')?.clientWidth ?? 0;
|
||||
const recContainerWidth = document.getElementById('rec-container')?.clientWidth ?? 0;
|
||||
const subjectDetailsContainer = document.getElementById('subject-details-container')?.clientWidth ?? 0;
|
||||
|
||||
return (subjectContainerWidth - recContainerWidth - subjectDetailsContainer) / 2;
|
||||
};
|
||||
const COMPONENTS = [
|
||||
{
|
||||
Component: SubjectText,
|
||||
id: 'subject'
|
||||
},
|
||||
{
|
||||
Component: ConferenceTimer,
|
||||
id: 'conference-timer'
|
||||
},
|
||||
{
|
||||
Component: ParticipantsCount,
|
||||
id: 'participants-count'
|
||||
},
|
||||
{
|
||||
Component: E2EELabel,
|
||||
id: 'e2ee'
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</>
|
||||
),
|
||||
id: 'recording'
|
||||
},
|
||||
{
|
||||
Component: LocalRecordingLabel,
|
||||
id: 'local-recording'
|
||||
},
|
||||
{
|
||||
Component: TranscribingLabel,
|
||||
id: 'transcribing'
|
||||
},
|
||||
{
|
||||
Component: VideoQualityLabel,
|
||||
id: 'video-quality'
|
||||
},
|
||||
{
|
||||
Component: InsecureRoomNameLabel,
|
||||
id: 'insecure-room'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* The upper band of the meeing containing the conference name, timer and labels.
|
||||
@@ -79,61 +84,90 @@ const getLeftMargin = () => {
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$None}
|
||||
*/
|
||||
function ConferenceInfo(props: Props) {
|
||||
const {
|
||||
_hideConferenceNameAndTimer,
|
||||
_hideConferenceTimer,
|
||||
_showParticipantCount,
|
||||
_hideRecordingLabel,
|
||||
_subject,
|
||||
_fullWidth,
|
||||
_visible,
|
||||
_recordingLabel
|
||||
} = props;
|
||||
class ConferenceInfo extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ConferenceInfo} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
return (
|
||||
<div className = { `subject ${_recordingLabel ? 'recording' : ''} ${_visible ? 'visible' : ''}` }>
|
||||
<div
|
||||
className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }
|
||||
id = 'subject-container'>
|
||||
{!_hideRecordingLabel && <div
|
||||
className = 'show-always'
|
||||
id = 'rec-container'
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
style = {{
|
||||
marginLeft: !_recordingLabel || _visible ? 0 : getLeftMargin()
|
||||
}}>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<LocalRecordingLabel />
|
||||
</div>
|
||||
this._renderAutoHide = this._renderAutoHide.bind(this);
|
||||
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
|
||||
}
|
||||
|
||||
_renderAutoHide: () => void;
|
||||
|
||||
/**
|
||||
* Renders auto-hidden info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAutoHide() {
|
||||
const { autoHide } = this.props._conferenceInfo;
|
||||
|
||||
if (!autoHide || !autoHide.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer visible = { this.props._visible } >
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => autoHide.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
<div
|
||||
className = 'subject-details-container'
|
||||
id = 'subject-details-container'>
|
||||
{
|
||||
!_hideConferenceNameAndTimer
|
||||
&& <div className = 'subject-info'>
|
||||
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
|
||||
{ !_hideConferenceTimer && <ConferenceTimer /> }
|
||||
</div>
|
||||
}
|
||||
{ _showParticipantCount && <ParticipantsCount /> }
|
||||
<E2EELabel />
|
||||
{_hideRecordingLabel && (
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<LocalRecordingLabel />
|
||||
</>
|
||||
)}
|
||||
<TranscribingLabel />
|
||||
<VideoQualityLabel />
|
||||
<InsecureRoomNameLabel />
|
||||
</div>
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
_renderAlwaysVisible: () => void;
|
||||
|
||||
/**
|
||||
* Renders the always visible info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAlwaysVisible() {
|
||||
const { alwaysVisible } = this.props._conferenceInfo;
|
||||
|
||||
if (!alwaysVisible || !alwaysVisible.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer visible = { true } >
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => alwaysVisible.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = 'details-container' >
|
||||
{ [
|
||||
this._renderAlwaysVisible(),
|
||||
this._renderAutoHide()
|
||||
] }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,40 +177,14 @@ function ConferenceInfo(props: Props) {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _hideConferenceTimer: boolean,
|
||||
* _showParticipantCount: boolean,
|
||||
* _subject: string,
|
||||
* _visible: boolean
|
||||
* _visible: boolean,
|
||||
* _conferenceInfo: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const participantCount = getParticipantCount(state);
|
||||
const {
|
||||
hideConferenceTimer,
|
||||
hideConferenceSubject,
|
||||
hideParticipantsStats,
|
||||
hideRecordingLabel,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
const shouldHideRecordingLabel = hideRecordingLabel || iAmRecorder;
|
||||
const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE);
|
||||
const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM);
|
||||
const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
|
||||
const isStreamRecording = streamRecordingStatus
|
||||
? streamRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
|
||||
const { isEngaged } = state['features/local-recording'];
|
||||
|
||||
return {
|
||||
_hideConferenceNameAndTimer: clientWidth < 300,
|
||||
_hideConferenceTimer: Boolean(hideConferenceTimer),
|
||||
_hideRecordingLabel: shouldHideRecordingLabel,
|
||||
_fullWidth: state['features/video-layout'].tileViewEnabled,
|
||||
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
|
||||
_subject: hideConferenceSubject ? '' : getConferenceName(state),
|
||||
_visible: isToolboxVisible(state),
|
||||
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !shouldHideRecordingLabel
|
||||
_conferenceInfo: getConferenceInfo(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The children components.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Whether this conference info container should be visible or not.
|
||||
*/
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export default ({ visible, children }: Props) => (
|
||||
<div className = { `subject${visible ? ' visible' : ''}` }>
|
||||
<div className = { 'subject-info-container' }>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -19,7 +19,7 @@ type Props = {
|
||||
/**
|
||||
* Number of the conference participants.
|
||||
*/
|
||||
count: string,
|
||||
count: number,
|
||||
|
||||
/**
|
||||
* Conference data.
|
||||
@@ -72,6 +72,12 @@ class ParticipantsCount extends PureComponent<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { count } = this.props;
|
||||
|
||||
if (count <= 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'participants-count'
|
||||
@@ -79,7 +85,7 @@ class ParticipantsCount extends PureComponent<Props> {
|
||||
<Label
|
||||
className = 'label--white'
|
||||
icon = { IconUserGroups }
|
||||
text = { this.props.count } />
|
||||
text = { count } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
50
react/features/conference/components/web/SubjectText.js
Normal file
50
react/features/conference/components/web/SubjectText.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The conference display name.
|
||||
*/
|
||||
_subject: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Label for the conference name.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const SubjectText = ({ _subject }: Props) => (
|
||||
<div className = 'subject-text'>
|
||||
<Tooltip
|
||||
content = { _subject }
|
||||
position = 'bottom'>
|
||||
<div className = 'subject-text--content'>{ _subject }</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code Subject}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _subject: string,
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_subject: getConferenceName(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(SubjectText);
|
||||
@@ -5,3 +5,4 @@ export { default as renderConferenceTimer } from './ConferenceTimerDisplay';
|
||||
export { default as InsecureRoomNameLabel } from './InsecureRoomNameLabel';
|
||||
export { default as InviteMore } from './InviteMore';
|
||||
export { default as ConferenceInfo } from './ConferenceInfo';
|
||||
export { default as SubjectText } from './SubjectText';
|
||||
|
||||
@@ -25,3 +25,12 @@ export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE';
|
||||
* }
|
||||
*/
|
||||
export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value E2EE maxMode.
|
||||
*
|
||||
* {
|
||||
* type: SET_MAX_MODE
|
||||
* }
|
||||
*/
|
||||
export const SET_MAX_MODE = 'SET_MAX_MODE';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, TOGGLE_E2EE } from './actionTypes';
|
||||
import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, SET_MAX_MODE, TOGGLE_E2EE } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to enable / disable E2EE.
|
||||
@@ -46,3 +46,16 @@ export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) {
|
||||
everyoneSupportE2EE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set E2EE maxMode.
|
||||
*
|
||||
* @param {string} maxMode - The new value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setE2EEMaxMode(maxMode: string) {
|
||||
return {
|
||||
type: SET_MAX_MODE,
|
||||
maxMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,17 +8,23 @@ import { translate } from '../../base/i18n';
|
||||
import { Switch } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { toggleE2EE } from '../actions';
|
||||
import { MAX_MODE } from '../constants';
|
||||
import { doesEveryoneSupportE2EE } from '../functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The resource for the description, computed based on the maxMode and whether the switch is toggled or not.
|
||||
*/
|
||||
_descriptionResource: string,
|
||||
|
||||
/**
|
||||
* Custom e2ee labels.
|
||||
*/
|
||||
_e2eeLabels: Object,
|
||||
|
||||
/**
|
||||
* Whether E2EE is currently enabled or not.
|
||||
* Whether the switch is currently enabled or not.
|
||||
*/
|
||||
_enabled: boolean,
|
||||
|
||||
@@ -27,6 +33,11 @@ type Props = {
|
||||
*/
|
||||
_everyoneSupportE2EE: boolean,
|
||||
|
||||
/**
|
||||
* Whether E2EE is currently enabled or not.
|
||||
*/
|
||||
_toggled: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
@@ -43,7 +54,7 @@ type State = {
|
||||
/**
|
||||
* True if the switch is toggled on.
|
||||
*/
|
||||
enabled: boolean
|
||||
toggled: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -59,10 +70,10 @@ class E2EESection extends Component<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: Props, state: Object) {
|
||||
if (props._enabled !== state.enabled) {
|
||||
if (props._toggled !== state.toggled) {
|
||||
|
||||
return {
|
||||
enabled: props._enabled
|
||||
toggled: props._toggled
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,7 +89,7 @@ class E2EESection extends Component<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enabled: false
|
||||
toggled: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
@@ -92,9 +103,9 @@ class E2EESection extends Component<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _e2eeLabels, _everyoneSupportE2EE, t } = this.props;
|
||||
const { enabled } = this.state;
|
||||
const description = _e2eeLabels?.description || t('dialog.e2eeDescription');
|
||||
const { _descriptionResource, _enabled, _e2eeLabels, _everyoneSupportE2EE, t } = this.props;
|
||||
const { toggled } = this.state;
|
||||
const description = _e2eeLabels?.description || t(_descriptionResource);
|
||||
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
|
||||
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
|
||||
|
||||
@@ -113,9 +124,10 @@ class E2EESection extends Component<Props, State> {
|
||||
{ label }
|
||||
</label>
|
||||
<Switch
|
||||
disabled = { !_enabled }
|
||||
id = 'e2ee-section-switch'
|
||||
onValueChange = { this._onToggle }
|
||||
value = { enabled } />
|
||||
value = { toggled } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -130,10 +142,10 @@ class E2EESection extends Component<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggle() {
|
||||
const newValue = !this.state.enabled;
|
||||
const newValue = !this.state.toggled;
|
||||
|
||||
this.setState({
|
||||
enabled: newValue
|
||||
toggled: newValue
|
||||
});
|
||||
|
||||
sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`));
|
||||
@@ -149,12 +161,28 @@ class E2EESection extends Component<Props, State> {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { enabled } = state['features/e2ee'];
|
||||
const { enabled: e2eeEnabled, maxMode } = state['features/e2ee'];
|
||||
const { e2eeLabels } = state['features/base/config'];
|
||||
|
||||
let descriptionResource = '';
|
||||
|
||||
if (e2eeLabels) {
|
||||
// When e2eeLabels are present, the descriptionResouse is ignored.
|
||||
descriptionResource = undefined;
|
||||
} else if (maxMode === MAX_MODE.THRESHOLD_EXCEEDED) {
|
||||
descriptionResource = 'dialog.e2eeDisabledDueToMaxModeDescription';
|
||||
} else if (maxMode === MAX_MODE.ENABLED) {
|
||||
descriptionResource = e2eeEnabled
|
||||
? 'dialog.e2eeWillDisableDueToMaxModeDescription' : 'dialog.e2eeDisabledDueToMaxModeDescription';
|
||||
} else {
|
||||
descriptionResource = 'dialog.e2eeDescription';
|
||||
}
|
||||
|
||||
return {
|
||||
_descriptionResource: descriptionResource,
|
||||
_e2eeLabels: e2eeLabels,
|
||||
_enabled: enabled,
|
||||
_enabled: maxMode === MAX_MODE.DISABLED || e2eeEnabled,
|
||||
_toggled: e2eeEnabled,
|
||||
_everyoneSupportE2EE: doesEveryoneSupportE2EE(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,3 +13,43 @@ export const E2EE_OFF_SOUND_ID = 'E2EE_OFF_SOUND';
|
||||
* @type {string}
|
||||
*/
|
||||
export const E2EE_ON_SOUND_ID = 'E2EE_ON_SOUND';
|
||||
|
||||
/**
|
||||
* The number of participants after which e2ee maxMode is set to MAX_MODE.ENABLED.
|
||||
*
|
||||
* @type {integer}
|
||||
*/
|
||||
export const MAX_MODE_LIMIT = 20;
|
||||
|
||||
/**
|
||||
* If the number of participants is greater then MAX_MODE_LIMIT + MAX_MODE_THRESHOLD
|
||||
* e2ee maxMode is set to MAX_MODE.THRESHOLD_EXCEEDED.
|
||||
*
|
||||
* @type {integer}
|
||||
*/
|
||||
export const MAX_MODE_THRESHOLD = 5;
|
||||
|
||||
export const MAX_MODE = {
|
||||
/**
|
||||
* Mode for which the e2ee can be enabled or disabled.
|
||||
* If e2ee is enabled, e2ee section is enabled with a warning text.
|
||||
* If e2ee is disabled, e2ee section is disabled with a warning text.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
ENABLED: 'max-mode-enabled',
|
||||
|
||||
/**
|
||||
* Mode for which the e2ee and the e2ee section are automatically disabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
THRESHOLD_EXCEEDED: 'max-mode-threshold-exceeded',
|
||||
|
||||
/**
|
||||
* The default e2ee maxMode, e2ee can be enabled/disabled, e2ee section is enabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
DISABLED: 'max-mode-disabled'
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { getParticipantCount } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants';
|
||||
|
||||
/**
|
||||
* Gets the value of a specific React {@code Component} prop of the currently
|
||||
* mounted {@link App}.
|
||||
@@ -27,3 +29,29 @@ export function doesEveryoneSupportE2EE(stateful) {
|
||||
|
||||
return everyoneSupportE2EE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT}.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean}.
|
||||
*/
|
||||
export function isMaxModeReached(stateful) {
|
||||
const participantCount = getParticipantCount(toState(stateful));
|
||||
|
||||
return participantCount >= MAX_MODE_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT + MAX_MODE_THREHOLD}.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean}.
|
||||
*/
|
||||
export function isMaxModeThresholdReached(stateful) {
|
||||
const participantCount = getParticipantCount(toState(stateful));
|
||||
|
||||
return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { CONFERENCE_JOINED, getCurrentConference } from '../base/conference';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
@@ -18,8 +18,9 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
|
||||
import { TOGGLE_E2EE } from './actionTypes';
|
||||
import { setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
|
||||
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
|
||||
import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
|
||||
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
|
||||
import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
|
||||
import logger from './logger';
|
||||
import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
|
||||
|
||||
@@ -46,6 +47,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(unregisterSound(E2EE_ON_SOUND_ID));
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
break;
|
||||
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { id, e2eeEnabled, e2eeSupported } = action.participant;
|
||||
const oldParticipant = getParticipantById(getState(), id);
|
||||
@@ -88,7 +94,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const result = next(action);
|
||||
const { e2eeEnabled, e2eeSupported, local } = action.participant;
|
||||
const { everyoneEnabledE2EE } = getState()['features/e2ee'];
|
||||
const participantCount = getParticipantCount(getState());
|
||||
const participantCount = getParticipantCount(getState);
|
||||
|
||||
// the initial values
|
||||
if (participantCount === 1) {
|
||||
@@ -116,6 +122,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(setEveryoneSupportE2EE(false));
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -165,6 +173,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
});
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -207,3 +217,24 @@ StateListenerRegistry.register(
|
||||
dispatch(toggleE2EE(false));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the maxMode based on the number of participants in the conference.
|
||||
*
|
||||
* @param { Dispatch<any>} dispatch - The redux dispatch function.
|
||||
* @param {Function|Object} getState - The {@code getState} function.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateMaxMode(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
||||
if (isMaxModeThresholdReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED));
|
||||
dispatch(toggleE2EE(false));
|
||||
} else if (isMaxModeReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.ENABLED));
|
||||
} else {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.DISABLED));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import { ReducerRegistry } from '../base/redux';
|
||||
import {
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
TOGGLE_E2EE
|
||||
} from './actionTypes';
|
||||
import { MAX_MODE } from './constants';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
enabled: false
|
||||
enabled: false,
|
||||
maxMode: MAX_MODE.DISABLED
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +36,13 @@ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
|
||||
everyoneSupportE2EE: action.everyoneSupportE2EE
|
||||
};
|
||||
|
||||
case SET_MAX_MODE: {
|
||||
return {
|
||||
...state,
|
||||
maxMode: action.maxMode
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,9 @@ class Thumbnail extends PureComponent<Props> {
|
||||
const indicators = [];
|
||||
|
||||
if (renderModeratorIndicator) {
|
||||
indicators.push(<View style = { styles.moderatorIndicatorContainer }>
|
||||
indicators.push(<View
|
||||
key = 'moderator-indicator'
|
||||
style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View>);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ type Props = {
|
||||
*/
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether this is a recorder or not.
|
||||
*/
|
||||
_iAmRecorder: boolean,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip button is enabled.
|
||||
*/
|
||||
@@ -235,14 +240,14 @@ class Filmstrip extends PureComponent <Props> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
_calculateIndices(startIndex, stopIndex) {
|
||||
const { _currentLayout, _thumbnailsReordered } = this.props;
|
||||
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
|
||||
let start = startIndex;
|
||||
let stop = stopIndex;
|
||||
|
||||
if (_thumbnailsReordered) {
|
||||
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
|
||||
// endpoint. The remote participants start from index 1.
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
start = Math.max(startIndex - 1, 0);
|
||||
stop = stopIndex - 1;
|
||||
}
|
||||
@@ -294,18 +299,24 @@ class Filmstrip extends PureComponent <Props> {
|
||||
* @returns {string} - The key.
|
||||
*/
|
||||
_gridItemKey({ columnIndex, rowIndex }) {
|
||||
const { _columns, _remoteParticipants, _remoteParticipantsLength, _thumbnailsReordered } = this.props;
|
||||
const {
|
||||
_columns,
|
||||
_iAmRecorder,
|
||||
_remoteParticipants,
|
||||
_remoteParticipantsLength,
|
||||
_thumbnailsReordered
|
||||
} = this.props;
|
||||
const index = (rowIndex * _columns) + columnIndex;
|
||||
|
||||
// When the thumbnails are reordered, local participant is inserted at index 0.
|
||||
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
|
||||
const remoteIndex = _thumbnailsReordered ? index - 1 : index;
|
||||
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
|
||||
|
||||
if (index > _remoteParticipantsLength) {
|
||||
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
|
||||
return `empty-${index}`;
|
||||
}
|
||||
|
||||
if (index === localIndex) {
|
||||
if (!_iAmRecorder && index === localIndex) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
@@ -528,7 +539,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const { testing = {}, iAmRecorder } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
const { visible, remoteParticipants } = state['features/filmstrip'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
@@ -596,6 +607,7 @@ function _mapStateToProps(state) {
|
||||
_currentLayout,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_iAmRecorder: Boolean(iAmRecorder),
|
||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_remoteParticipantsLength: remoteParticipants.length,
|
||||
_remoteParticipants: remoteParticipants,
|
||||
|
||||
@@ -129,11 +129,13 @@ function _mapStateToProps(state, ownProps) {
|
||||
isAudioMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID);
|
||||
}
|
||||
|
||||
const { disableModeratorIndicator } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_currentLayout: getCurrentLayout(state),
|
||||
_showAudioMutedIndicator: isAudioMuted,
|
||||
_showModeratorIndicator:
|
||||
!interfaceConfig.DISABLE_FOCUS_INDICATOR && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
|
||||
!disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
|
||||
_showScreenShareIndicator: isScreenSharing,
|
||||
_showVideoMutedIndicator: isVideoMuted
|
||||
};
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
DISPLAY_MODE_TO_STRING,
|
||||
DISPLAY_VIDEO,
|
||||
DISPLAY_VIDEO_WITH_NAME,
|
||||
MOBILE_FILMSTRIP_PORTRAIT_RATIO,
|
||||
VIDEO_TEST_EVENTS,
|
||||
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
|
||||
} from '../../constants';
|
||||
@@ -771,7 +770,6 @@ class Thumbnail extends Component<Props, State> {
|
||||
const {
|
||||
_defaultLocalDisplayName,
|
||||
_disableLocalVideoFlip,
|
||||
_height,
|
||||
_isMobile,
|
||||
_isMobilePortrait,
|
||||
_isScreenSharing,
|
||||
@@ -783,13 +781,14 @@ class Thumbnail extends Component<Props, State> {
|
||||
const { id } = _participant || {};
|
||||
const { audioLevel } = this.state;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
let containerClassName = this._getContainerClassName();
|
||||
const videoTrackClassName
|
||||
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
|
||||
|
||||
styles.thumbnail.height = _isMobilePortrait
|
||||
? `${Math.floor(_height * MOBILE_FILMSTRIP_PORTRAIT_RATIO)}px`
|
||||
: styles.thumbnail.height;
|
||||
if (_isMobilePortrait) {
|
||||
styles.thumbnail.height = styles.thumbnail.width;
|
||||
containerClassName = `${containerClassName} self-view-mobile-portrait`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -113,26 +113,27 @@ function _mapStateToProps(state, ownProps) {
|
||||
const { columns, rows } = gridDimensions;
|
||||
const index = (rowIndex * columns) + columnIndex;
|
||||
let horizontalOffset;
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1);
|
||||
|
||||
if (rowIndex === rows - 1) { // center the last row
|
||||
const { width: thumbnailWidth } = thumbnailSize;
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const partialLastRowParticipantsNumber = (remoteParticipantsLength + (iAmRecorder ? 0 : 1)) % columns;
|
||||
const partialLastRowParticipantsNumber = participantsLenght % columns;
|
||||
|
||||
if (partialLastRowParticipantsNumber > 0) {
|
||||
horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (index > remoteParticipantsLength) {
|
||||
if (index > participantsLenght - 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// When the thumbnails are reordered, local participant is inserted at index 0.
|
||||
const localIndex = enableThumbnailReordering ? 0 : remoteParticipantsLength;
|
||||
const remoteIndex = enableThumbnailReordering ? index - 1 : index;
|
||||
const remoteIndex = enableThumbnailReordering && !iAmRecorder ? index - 1 : index;
|
||||
|
||||
if (index === localIndex) {
|
||||
if (!iAmRecorder && index === localIndex) {
|
||||
return {
|
||||
_participantID: 'local',
|
||||
_horizontalOffset: horizontalOffset
|
||||
|
||||
@@ -221,14 +221,6 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
|
||||
*/
|
||||
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
|
||||
|
||||
|
||||
/**
|
||||
* The ratio for filmstrip self view on mobile portrait mode.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const MOBILE_FILMSTRIP_PORTRAIT_RATIO = 2.5;
|
||||
|
||||
/**
|
||||
* The margin for each side of the tile view. Taken away from the available
|
||||
* height and width for the tile container to display in.
|
||||
|
||||
@@ -13,15 +13,20 @@ import { Tooltip } from '../../base/tooltip';
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether this is the Jibri recorder participant.
|
||||
*/
|
||||
_iAmRecorder: boolean,
|
||||
|
||||
/**
|
||||
* Whether local recording is engaged or not.
|
||||
*/
|
||||
_isEngaged: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Whether local recording is engaged or not.
|
||||
*/
|
||||
isEngaged: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,7 +43,7 @@ class LocalRecordingLabel extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.isEngaged) {
|
||||
if (!this.props._isEngaged || this.props._iAmRecorder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -66,9 +71,11 @@ class LocalRecordingLabel extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { isEngaged } = state['features/local-recording'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
isEngaged
|
||||
_isEngaged: isEngaged,
|
||||
_iAmRecorder: iAmRecorder
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,13 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
|
||||
for (const device of devices) {
|
||||
const infoMap = deviceInfoMap[device.type];
|
||||
|
||||
// Skip devices with unknown type.
|
||||
if (!infoMap) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = device.type === 'BLUETOOTH' && device.name ? device.name : infoMap.text;
|
||||
|
||||
if (infoMap) {
|
||||
|
||||
@@ -12,6 +12,11 @@ export type Props = {
|
||||
*/
|
||||
appearance: string,
|
||||
|
||||
/**
|
||||
* Whether or not the title and description should be concatenated.
|
||||
*/
|
||||
concatText?: boolean,
|
||||
|
||||
/**
|
||||
* Callback invoked when the custom button is clicked.
|
||||
*/
|
||||
|
||||
@@ -66,12 +66,17 @@ class Notification extends AbstractNotification<Props> {
|
||||
* @private
|
||||
*/
|
||||
_renderContent() {
|
||||
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey } = this.props;
|
||||
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey, concatText } = this.props;
|
||||
const titleText = title || (titleKey && t(titleKey, titleArguments));
|
||||
const description = this._getDescription();
|
||||
const titleConcat = [];
|
||||
|
||||
if (concatText) {
|
||||
titleConcat.push(titleText);
|
||||
}
|
||||
|
||||
if (description && description.length) {
|
||||
return description.map((line, index) => (
|
||||
return [ ...titleConcat, ...description ].map((line, index) => (
|
||||
<Text
|
||||
key = { index }
|
||||
numberOfLines = { maxLines }
|
||||
|
||||
@@ -69,13 +69,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
if (typeof interfaceConfig === 'undefined') {
|
||||
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
||||
const state = store.getState();
|
||||
const { disableModeratorIndicator } = state['features/base/config'];
|
||||
|
||||
if (disableModeratorIndicator) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { id, role } = action.participant;
|
||||
const state = store.getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (localParticipant.id !== id) {
|
||||
|
||||
@@ -2,20 +2,29 @@
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
import { Divider, Text } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
requestDisableAudioModeration,
|
||||
requestDisableVideoModeration,
|
||||
requestEnableAudioModeration,
|
||||
requestEnableVideoModeration
|
||||
} from '../../../av-moderation/actions';
|
||||
import {
|
||||
isSupported as isAvModerationSupported,
|
||||
isEnabled as isAvModerationEnabled
|
||||
} from '../../../av-moderation/functions';
|
||||
import { openDialog, hideDialog } from '../../../base/dialog/actions';
|
||||
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
|
||||
import {
|
||||
Icon,
|
||||
IconCheck,
|
||||
IconVideoOff
|
||||
} from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount
|
||||
} from '../../../base/participants';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getParticipantCount, isEveryoneModerator } from '../../../base/participants';
|
||||
import MuteEveryonesVideoDialog
|
||||
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
|
||||
|
||||
@@ -24,21 +33,29 @@ import styles from './styles';
|
||||
export const ContextMenuMore = () => {
|
||||
const dispatch = useDispatch();
|
||||
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
|
||||
const { id } = useSelector(getLocalParticipant);
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
const showSlidingView = participantsCount > 2;
|
||||
const muteAllVideo = useCallback(() =>
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog,
|
||||
{ exclude: [ id ] })),
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog)),
|
||||
[ dispatch ]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isModerationSupported = useSelector(isAvModerationSupported());
|
||||
const allModerators = useSelector(isEveryoneModerator);
|
||||
const participantCount = useSelector(getParticipantCount);
|
||||
|
||||
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
||||
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
|
||||
|
||||
const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
|
||||
const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
|
||||
|
||||
const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
|
||||
const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
addScrollViewPadding = { false }
|
||||
onCancel = { cancel }
|
||||
showSlidingView = { showSlidingView }
|
||||
style = { styles.contextMenuMore }>
|
||||
showSlidingView = { true }>
|
||||
<TouchableOpacity
|
||||
onPress = { muteAllVideo }
|
||||
style = { styles.contextMenuItem }>
|
||||
@@ -47,6 +64,48 @@ export const ContextMenuMore = () => {
|
||||
src = { IconVideoOff } />
|
||||
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
|
||||
</TouchableOpacity>
|
||||
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
|
||||
<Divider style = { styles.divider } />
|
||||
<View style = { styles.contextMenuItem }>
|
||||
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.allow')}</Text>
|
||||
</View>
|
||||
{isAudioModerationEnabled
|
||||
? <TouchableOpacity
|
||||
onPress = { disableAudioModeration }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Text style = { styles.contextMenuItemTextNoIcon }>
|
||||
{t('participantsPane.actions.audioModeration')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
: <TouchableOpacity
|
||||
onPress = { enableAudioModeration }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconCheck } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{t('participantsPane.actions.audioModeration')}
|
||||
</Text>
|
||||
</TouchableOpacity> }
|
||||
{isVideoModerationEnabled
|
||||
? <TouchableOpacity
|
||||
onPress = { disableVideoModeration }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Text style = { styles.contextMenuItemTextNoIcon }>
|
||||
{t('participantsPane.actions.videoModeration')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
: <TouchableOpacity
|
||||
onPress = { enableVideoModeration }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconCheck } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{t('participantsPane.actions.videoModeration')}
|
||||
</Text>
|
||||
</TouchableOpacity>}
|
||||
</>}
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconHorizontalPoints } from '../../../base/icons';
|
||||
|
||||
const HorizontalDotsIcon = () => (<Icon
|
||||
size = { 20 }
|
||||
src = { IconHorizontalPoints } />);
|
||||
|
||||
export default HorizontalDotsIcon;
|
||||
@@ -6,7 +6,8 @@ import { translate } from '../../../base/i18n';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantByIdOrUndefined,
|
||||
getParticipantDisplayName
|
||||
getParticipantDisplayName,
|
||||
isParticipantModerator
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
@@ -14,9 +15,8 @@ import {
|
||||
isParticipantVideoMuted
|
||||
} from '../../../base/tracks';
|
||||
import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
|
||||
import { MEDIA_STATE } from '../../constants';
|
||||
import type { MediaState } from '../../constants';
|
||||
import { getParticipantAudioMediaState } from '../../functions';
|
||||
import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
|
||||
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
|
||||
@@ -39,9 +39,9 @@ type Props = {
|
||||
_isFakeParticipant: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is video muted.
|
||||
* Whether or not the user is a moderator.
|
||||
*/
|
||||
_isVideoMuted: boolean,
|
||||
_isModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is the local participant.
|
||||
@@ -63,6 +63,11 @@ type Props = {
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Media state for video.
|
||||
*/
|
||||
_videoMediaState: MediaState,
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
@@ -127,10 +132,11 @@ class MeetingParticipantItem extends PureComponent<Props> {
|
||||
const {
|
||||
_audioMediaState,
|
||||
_displayName,
|
||||
_isVideoMuted,
|
||||
_isModerator,
|
||||
_local,
|
||||
_participantID,
|
||||
_raisedHand
|
||||
_raisedHand,
|
||||
_videoMediaState
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -138,11 +144,12 @@ class MeetingParticipantItem extends PureComponent<Props> {
|
||||
audioMediaState = { _audioMediaState }
|
||||
displayName = { _displayName }
|
||||
isKnockingParticipant = { false }
|
||||
isModerator = { _isModerator }
|
||||
local = { _local }
|
||||
onPress = { this._onPress }
|
||||
participantID = { _participantID }
|
||||
raisedHand = { _raisedHand }
|
||||
videoMediaState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } />
|
||||
videoMediaState = { _videoMediaState } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -161,19 +168,21 @@ function mapStateToProps(state, ownProps): Object {
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||
const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
|
||||
|
||||
return {
|
||||
_audioMediaState: audioMediaState,
|
||||
_displayName: getParticipantDisplayName(state, participant?.id),
|
||||
_isAudioMuted,
|
||||
_isFakeParticipant: Boolean(participant?.isFakeParticipant),
|
||||
_isVideoMuted: isVideoMuted,
|
||||
_isModerator: isParticipantModerator(participant),
|
||||
_local: Boolean(participant?.local),
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participantID: participant?.id,
|
||||
_raisedHand: Boolean(participant?.raisedHand)
|
||||
_raisedHand: Boolean(participant?.raisedHand),
|
||||
_videoMediaState: videoMediaState
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ type Props = {
|
||||
*/
|
||||
isKnockingParticipant: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the user is a moderator.
|
||||
*/
|
||||
isModerator?: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is local.
|
||||
*/
|
||||
@@ -69,6 +74,7 @@ function ParticipantItem({
|
||||
children,
|
||||
displayName,
|
||||
isKnockingParticipant,
|
||||
isModerator,
|
||||
local,
|
||||
onPress,
|
||||
participantID,
|
||||
@@ -88,11 +94,14 @@ function ParticipantItem({
|
||||
className = 'participant-avatar'
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<View style = { styles.participantNameContainer }>
|
||||
<Text style = { styles.participantName }>
|
||||
{ displayName }
|
||||
</Text>
|
||||
{ local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
|
||||
<View style = { styles.participantDetailsContainer }>
|
||||
<View style = { styles.participantNameContainer }>
|
||||
<Text style = { styles.participantName }>
|
||||
{ displayName }
|
||||
</Text>
|
||||
{ local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
|
||||
</View>
|
||||
{isModerator && <Text style = { styles.moderatorLabel }>{t('videothumbnail.moderator')}</Text>}
|
||||
</View>
|
||||
{
|
||||
!isKnockingParticipant
|
||||
|
||||
@@ -7,10 +7,8 @@ import { Button } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { Icon, IconHorizontalPoints } from '../../../base/icons';
|
||||
import { JitsiModal } from '../../../base/modal';
|
||||
import {
|
||||
getParticipantCount,
|
||||
isLocalParticipantModerator
|
||||
} from '../../../base/participants';
|
||||
import MuteEveryoneDialog
|
||||
@@ -18,6 +16,7 @@ import MuteEveryoneDialog
|
||||
import { close } from '../../actions.native';
|
||||
|
||||
import { ContextMenuMore } from './ContextMenuMore';
|
||||
import HorizontalDotsIcon from './HorizontalDotsIcon';
|
||||
import LobbyParticipantList from './LobbyParticipantList';
|
||||
import MeetingParticipantList from './MeetingParticipantList';
|
||||
import styles from './styles';
|
||||
@@ -32,8 +31,6 @@ const ParticipantsPane = () => {
|
||||
const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]);
|
||||
const closePane = useCallback(() => dispatch(close()), [ dispatch ]);
|
||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
const showContextMenu = participantsCount > 2;
|
||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
|
||||
[ dispatch ]);
|
||||
const { t } = useTranslation();
|
||||
@@ -55,21 +52,13 @@ const ParticipantsPane = () => {
|
||||
labelStyle = { styles.muteAllLabel }
|
||||
mode = 'contained'
|
||||
onPress = { muteAll }
|
||||
style = { showContextMenu ? styles.muteAllMoreButton : styles.muteAllButton } />
|
||||
{
|
||||
showContextMenu
|
||||
&& <Button
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
icon = { () =>
|
||||
(<Icon
|
||||
size = { 20 }
|
||||
src = { IconHorizontalPoints } />)
|
||||
}
|
||||
labelStyle = { styles.moreIcon }
|
||||
mode = 'contained'
|
||||
onPress = { openMoreMenu }
|
||||
style = { styles.moreButton } />
|
||||
}
|
||||
style = { styles.muteAllMoreButton } />
|
||||
<Button
|
||||
icon = { HorizontalDotsIcon }
|
||||
labelStyle = { styles.moreIcon }
|
||||
mode = 'contained'
|
||||
onPress = { openMoreMenu }
|
||||
style = { styles.moreButton } />
|
||||
</View>
|
||||
}
|
||||
</JitsiModal>
|
||||
|
||||
@@ -78,8 +78,7 @@ const contextMenuItem = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[7],
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,12 +136,18 @@ export default {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantDetailsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantNameContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
width: '63%'
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantName: {
|
||||
@@ -150,6 +155,13 @@ export default {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
moderatorLabel: {
|
||||
color: BaseTheme.palette.text03,
|
||||
alignSelf: 'flex-start',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
isLocal: {
|
||||
alignSelf: 'center',
|
||||
color: BaseTheme.palette.text01,
|
||||
@@ -249,11 +261,6 @@ export default {
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
contextMenuMore: {
|
||||
backgroundColor: BaseTheme.palette.bottomSheet,
|
||||
borderRadius: BaseTheme.shape.borderRadius
|
||||
},
|
||||
|
||||
muteAllButton: {
|
||||
...muteAllButton
|
||||
},
|
||||
@@ -300,6 +307,11 @@ export default {
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
contextMenuItemTextNoIcon: {
|
||||
...contextMenuItemText,
|
||||
marginLeft: BaseTheme.spacing[6]
|
||||
},
|
||||
|
||||
contextMenuItemName: {
|
||||
color: BaseTheme.palette.text04,
|
||||
flexShrink: 1,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
@@ -135,6 +136,11 @@ type Props = {
|
||||
*/
|
||||
participantID: ?string,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The translated "you" text.
|
||||
*/
|
||||
@@ -167,6 +173,7 @@ function MeetingParticipantItem({
|
||||
openDrawerForParticipant,
|
||||
overflowDrawer,
|
||||
participantActionEllipsisLabel,
|
||||
t,
|
||||
youText
|
||||
}: Props) {
|
||||
|
||||
@@ -202,6 +209,12 @@ function MeetingParticipantItem({
|
||||
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
||||
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
||||
|
||||
let askToUnmuteText = askUnmuteText;
|
||||
|
||||
if (_audioMediaState !== MEDIA_STATE.FORCE_MUTED && _videoMediaState === MEDIA_STATE.FORCE_MUTED) {
|
||||
askToUnmuteText = t('participantsPane.actions.allowVideo');
|
||||
}
|
||||
|
||||
return (
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
@@ -221,7 +234,7 @@ function MeetingParticipantItem({
|
||||
{!overflowDrawer && !_participant?.isFakeParticipant
|
||||
&& <>
|
||||
<ParticipantQuickAction
|
||||
askUnmuteText = { askUnmuteText }
|
||||
askUnmuteText = { askToUnmuteText }
|
||||
buttonType = { _quickActionButtonType }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
@@ -280,4 +293,4 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(MeetingParticipantItem);
|
||||
export default translate(connect(_mapStateToProps)(MeetingParticipantItem));
|
||||
|
||||
@@ -71,7 +71,6 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
|
||||
* @param {Object} participant - The participant.
|
||||
* @param {boolean} muted - The mute state of the participant.
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||
@@ -98,7 +97,6 @@ export function getParticipantAudioMediaState(participant: Object, muted: Boolea
|
||||
* @param {Object} participant - The participant.
|
||||
* @param {boolean} muted - The mute state of the participant.
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { getLocalParticipant, participantUpdated } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
@@ -21,34 +22,39 @@ import ReactionButton from './ReactionButton';
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
* Docks the toolbox
|
||||
*/
|
||||
t: Function,
|
||||
_dockToolbox: Function,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
* Whether or not it's a mobile browser.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
_isMobile: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the local participant.
|
||||
*/
|
||||
_localParticipantID: String,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Docks the toolbox
|
||||
*/
|
||||
_dockToolbox: Function,
|
||||
|
||||
/**
|
||||
* Whether or not it's displayed in the overflow menu.
|
||||
*/
|
||||
overflowMenu: boolean
|
||||
overflowMenu: boolean,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
declare var APP: Object;
|
||||
@@ -177,25 +183,27 @@ class ReactionsMenu extends Component<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _raisedHand, t, overflowMenu } = this.props;
|
||||
const { _raisedHand, t, overflowMenu, _isMobile } = this.props;
|
||||
|
||||
return (
|
||||
<div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
|
||||
<div className = 'reactions-row'>
|
||||
{ this._getReactionButtons() }
|
||||
</div>
|
||||
<div className = 'raise-hand-row'>
|
||||
<ReactionButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = '✋'
|
||||
key = 'raisehand'
|
||||
label = {
|
||||
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
|
||||
${overflowMenu ? '' : ' (R)'}`
|
||||
}
|
||||
onClick = { this._onToolbarToggleRaiseHand }
|
||||
toggled = { true } />
|
||||
</div>
|
||||
{_isMobile && (
|
||||
<div className = 'raise-hand-row'>
|
||||
<ReactionButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = '✋'
|
||||
key = 'raisehand'
|
||||
label = {
|
||||
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
|
||||
${overflowMenu ? '' : ' (R)'}`
|
||||
}
|
||||
onClick = { this._onToolbarToggleRaiseHand }
|
||||
toggled = { true } />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -212,6 +220,7 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
_localParticipantID: localParticipant.id,
|
||||
_isMobile: isMobileBrowser(),
|
||||
_raisedHand: localParticipant.raisedHand
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconRaisedHand } from '../../../base/icons';
|
||||
import { IconArrowUp, IconRaisedHand } from '../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
|
||||
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
|
||||
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
||||
import { type ReactionEmojiProps } from '../../constants';
|
||||
import { getReactionsQueue } from '../../functions.any';
|
||||
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
|
||||
import { getReactionsMenuVisibility } from '../../functions.web';
|
||||
|
||||
import ReactionEmoji from './ReactionEmoji';
|
||||
@@ -18,34 +20,44 @@ import ReactionsMenuPopup from './ReactionsMenuPopup';
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
* Whether or not reactions are enabled.
|
||||
*/
|
||||
t: Function,
|
||||
_reactionsEnabled: Boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
raisedHand: boolean,
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Click handler for the reaction button. Toggles the reactions menu.
|
||||
* Click handler for raise hand functionality.
|
||||
*/
|
||||
onReactionsClick: Function,
|
||||
handleClick: Function,
|
||||
|
||||
/**
|
||||
* Whether or not the reactions menu is open.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not it's a mobile browser.
|
||||
*/
|
||||
isMobile: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
*/
|
||||
raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The array of reactions to be displayed.
|
||||
*/
|
||||
reactionsQueue: Array<ReactionEmojiProps>,
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
* Used for translation.
|
||||
*/
|
||||
dispatch: Function
|
||||
t: Function
|
||||
};
|
||||
|
||||
|
||||
@@ -57,11 +69,14 @@ declare var APP: Object;
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionsMenuButton({
|
||||
t,
|
||||
raisedHand,
|
||||
_reactionsEnabled,
|
||||
dispatch,
|
||||
handleClick,
|
||||
isOpen,
|
||||
isMobile,
|
||||
raisedHand,
|
||||
reactionsQueue,
|
||||
dispatch
|
||||
t
|
||||
}: Props) {
|
||||
|
||||
/**
|
||||
@@ -73,16 +88,32 @@ function ReactionsMenuButton({
|
||||
dispatch(toggleReactionsMenuVisibility());
|
||||
}
|
||||
|
||||
const raiseHandButton = (<ToolbarButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = { IconRaisedHand }
|
||||
key = 'raise-hand'
|
||||
onClick = { handleClick }
|
||||
toggled = { raisedHand }
|
||||
tooltip = { t('toolbar.raiseHand') } />);
|
||||
|
||||
return (
|
||||
<div className = 'reactions-menu-popup-container'>
|
||||
<ReactionsMenuPopup>
|
||||
<ToolbarButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||
icon = { IconRaisedHand }
|
||||
key = 'reactions'
|
||||
onClick = { toggleReactionsMenu }
|
||||
toggled = { raisedHand }
|
||||
tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
|
||||
{!_reactionsEnabled || isMobile ? raiseHandButton
|
||||
: (
|
||||
<ToolboxButtonWithIcon
|
||||
ariaControls = 'reactions-menu-dialog'
|
||||
ariaExpanded = { isOpen }
|
||||
ariaHasPopup = { true }
|
||||
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||
icon = { IconArrowUp }
|
||||
iconDisabled = { false }
|
||||
iconId = 'reactions-menu-button'
|
||||
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) }
|
||||
onIconClick = { toggleReactionsMenu }>
|
||||
{raiseHandButton}
|
||||
</ToolboxButtonWithIcon>
|
||||
)}
|
||||
</ReactionsMenuPopup>
|
||||
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||
index = { index }
|
||||
@@ -103,7 +134,9 @@ function mapStateToProps(state) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_reactionsEnabled: isReactionsEnabled(state),
|
||||
isOpen: getReactionsMenuVisibility(state),
|
||||
isMobile: isMobileBrowser(),
|
||||
reactionsQueue: getReactionsQueue(state),
|
||||
raisedHand: localParticipant?.raisedHand
|
||||
};
|
||||
|
||||
@@ -151,11 +151,11 @@ export function getReactionsSoundsThresholds(reactions: Array<string>) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isReactionsEnabled(state: Object) {
|
||||
const { enableReactions } = state['features/base/config'];
|
||||
const { disableReactions } = state['features/base/config'];
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return enableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
|
||||
return !disableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
|
||||
}
|
||||
|
||||
return enableReactions;
|
||||
return !disableReactions;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ export function showStartedRecordingNotification(
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(showNotification(dialogProps));
|
||||
dispatch(showNotification(dialogProps, NOTIFICATION_TIMEOUT));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ import { getSessionStatusToShow } from '../functions';
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether this is the Jibri recorder participant.
|
||||
*/
|
||||
_iAmRecorder: boolean,
|
||||
|
||||
/**
|
||||
* The status of the highermost priority session.
|
||||
*/
|
||||
@@ -100,7 +105,7 @@ export default class AbstractRecordingLabel
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return this.props._status && !this.state.staleLabel
|
||||
return this.props._status && !this.state.staleLabel && !this.props._iAmRecorder
|
||||
? this._renderLabel() : null;
|
||||
}
|
||||
|
||||
@@ -172,6 +177,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { mode } = ownProps;
|
||||
|
||||
return {
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_status: getSessionStatusToShow(state, mode)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ declare var APP: Object;
|
||||
export type Props = {
|
||||
...$Exact<AbstractDialogTabProps>,
|
||||
|
||||
/**
|
||||
* Whether or not the reactions feature is enabled.
|
||||
*/
|
||||
enableReactions: Boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the incoming message should play.
|
||||
*/
|
||||
@@ -40,11 +45,6 @@ export type Props = {
|
||||
*/
|
||||
soundsReactions: Boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the reactions feature is enabled.
|
||||
*/
|
||||
enableReactions: Boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { toState } from '../base/redux';
|
||||
import { parseStandardURIString } from '../base/util';
|
||||
import { isFollowMeActive } from '../follow-me';
|
||||
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
|
||||
|
||||
@@ -174,7 +175,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions
|
||||
} = state['features/base/settings'];
|
||||
const { enableReactions } = state['features/base/config'];
|
||||
const enableReactions = isReactionsEnabled(state);
|
||||
|
||||
return {
|
||||
soundsIncomingMessage,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconRaisedHand } from '../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation of a button for toggling raise hand functionality.
|
||||
*/
|
||||
class RaiseHandButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
|
||||
icon = IconRaisedHand
|
||||
label = 'toolbar.raiseYourHand';
|
||||
toggledLabel = 'toolbar.lowerYourHand'
|
||||
|
||||
/**
|
||||
* Retrieves tooltip dynamically.
|
||||
*/
|
||||
get tooltip() {
|
||||
return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand';
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by linter due to AbstractButton overwritten prop being writable.
|
||||
*
|
||||
* @param {string} value - The value.
|
||||
*/
|
||||
set tooltip(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return this.props._raisedHand;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
const mapStateToProps = state => {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_raisedHand: localParticipant.raisedHand
|
||||
};
|
||||
};
|
||||
|
||||
export default translate(connect(mapStateToProps)(RaiseHandButton));
|
||||
@@ -17,7 +17,6 @@ import { translate } from '../../../base/i18n';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount,
|
||||
haveParticipantWithScreenSharingFeature,
|
||||
raiseHand
|
||||
} from '../../../base/participants';
|
||||
@@ -87,7 +86,6 @@ import AudioSettingsButton from './AudioSettingsButton';
|
||||
import FullscreenButton from './FullscreenButton';
|
||||
import OverflowMenuButton from './OverflowMenuButton';
|
||||
import ProfileButton from './ProfileButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import Separator from './Separator';
|
||||
import ShareDesktopButton from './ShareDesktopButton';
|
||||
import ToggleCameraButton from './ToggleCameraButton';
|
||||
@@ -180,11 +178,6 @@ type Props = {
|
||||
*/
|
||||
_overflowMenuVisible: boolean,
|
||||
|
||||
/**
|
||||
* Number of participants in the conference.
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* Whether or not the participants pane is open.
|
||||
*/
|
||||
@@ -254,16 +247,12 @@ type Props = {
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
type State = {
|
||||
reactionsShortcutsRegistered: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements the conference toolbox on React/Web.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Toolbox extends Component<Props, State> {
|
||||
class Toolbox extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code Toolbox} instance.
|
||||
*
|
||||
@@ -273,10 +262,6 @@ class Toolbox extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reactionsShortcutsRegistered: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onMouseOut = this._onMouseOut.bind(this);
|
||||
this._onMouseOver = this._onMouseOver.bind(this);
|
||||
@@ -306,7 +291,7 @@ class Toolbox extends Component<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _participantCount } = this.props;
|
||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
|
||||
const KEYBOARD_SHORTCUTS = [
|
||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||
character: 'A',
|
||||
@@ -355,7 +340,7 @@ class Toolbox extends Component<Props, State> {
|
||||
}
|
||||
});
|
||||
|
||||
if (_reactionsEnabled && _participantCount > 1) {
|
||||
if (_reactionsEnabled) {
|
||||
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
|
||||
const onShortcutSendReaction = () => {
|
||||
dispatch(addReactionToBuffer(key));
|
||||
@@ -389,7 +374,7 @@ class Toolbox extends Component<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
const { _dialog, _reactionsEnabled, _participantCount, dispatch, t } = this.props;
|
||||
const { _dialog, dispatch } = this.props;
|
||||
|
||||
|
||||
if (prevProps._overflowMenuVisible
|
||||
@@ -398,41 +383,6 @@ class Toolbox extends Component<Props, State> {
|
||||
this._onSetOverflowVisible(false);
|
||||
dispatch(setToolbarHovered(false));
|
||||
}
|
||||
|
||||
if (!this.state.reactionsShortcutsRegistered
|
||||
&& (prevProps._reactionsEnabled !== _reactionsEnabled
|
||||
|| prevProps._participantCount !== _participantCount)) {
|
||||
if (_reactionsEnabled && _participantCount > 1) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
reactionsShortcutsRegistered: true
|
||||
});
|
||||
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
|
||||
const onShortcutSendReaction = () => {
|
||||
dispatch(addReactionToBuffer(key));
|
||||
sendAnalytics(createShortcutEvent(
|
||||
`reaction.${key}`
|
||||
));
|
||||
};
|
||||
|
||||
return {
|
||||
character: REACTIONS[key].shortcutChar,
|
||||
exec: onShortcutSendReaction,
|
||||
helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
|
||||
altKey: true
|
||||
};
|
||||
});
|
||||
|
||||
REACTION_SHORTCUTS.forEach(shortcut => {
|
||||
APP.keyboardshortcut.registerShortcut(
|
||||
shortcut.character,
|
||||
null,
|
||||
shortcut.exec,
|
||||
shortcut.helpDescription,
|
||||
shortcut.altKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,7 +395,7 @@ class Toolbox extends Component<Props, State> {
|
||||
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
|
||||
APP.keyboardshortcut.unregisterShortcut(letter));
|
||||
|
||||
if (this.props._reactionsEnabled && this.state.reactionsShortcutsRegistered) {
|
||||
if (this.props._reactionsEnabled) {
|
||||
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
|
||||
.forEach(letter =>
|
||||
APP.keyboardshortcut.unregisterShortcut(letter, true));
|
||||
@@ -613,8 +563,7 @@ class Toolbox extends Component<Props, State> {
|
||||
const {
|
||||
_feedbackConfigured,
|
||||
_isMobile,
|
||||
_screenSharing,
|
||||
_reactionsEnabled
|
||||
_screenSharing
|
||||
} = this.props;
|
||||
|
||||
const microphone = {
|
||||
@@ -651,8 +600,8 @@ class Toolbox extends Component<Props, State> {
|
||||
|
||||
const raisehand = {
|
||||
key: 'raisehand',
|
||||
Content: _reactionsEnabled ? ReactionsMenuButton : RaiseHandButton,
|
||||
handleClick: _reactionsEnabled ? null : this._onToolbarToggleRaiseHand,
|
||||
Content: ReactionsMenuButton,
|
||||
handleClick: this._onToolbarToggleRaiseHand,
|
||||
group: 2
|
||||
};
|
||||
|
||||
@@ -1388,7 +1337,6 @@ function _mapStateToProps(state, ownProps) {
|
||||
_localParticipantID: localParticipant?.id,
|
||||
_localVideo: localVideo,
|
||||
_overflowMenuVisible: overflowMenuVisible,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
||||
_raisedHand: localParticipant?.raisedHand,
|
||||
_reactionsEnabled: isReactionsEnabled(state),
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isBrowsersOptimal } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { CHROME, FIREFOX } from './browserLinks';
|
||||
|
||||
/**
|
||||
* The namespace of the CSS styles of UnsupportedDesktopBrowser.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _SNS = 'unsupported-desktop-browser';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link JaasUnsupportedDesktopBrowser}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing unsupported browser page.
|
||||
*
|
||||
* @class UnsupportedDesktopBrowser
|
||||
*/
|
||||
class JaasUnsupportedDesktopBrowser extends Component<Props> {
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = { _SNS }>
|
||||
<h2 className = { `${_SNS}__title` }>
|
||||
It looks like you're using a browser we don't support.
|
||||
</h2>
|
||||
<p className = { `${_SNS}__description` }>
|
||||
Please try again with the latest version of
|
||||
<a
|
||||
className = { `${_SNS}__link` }
|
||||
href = { CHROME } >Chrome</a>
|
||||
{
|
||||
this._showFirefox() && <>or <a
|
||||
className = { `${_SNS}__link` }
|
||||
href = { FIREFOX }>Firefox</a></>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a link to download Firefox is displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_showFirefox() {
|
||||
return isBrowsersOptimal('firefox');
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(JaasUnsupportedDesktopBrowser);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DefaultUnsupportedDesktopBrowser';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
|
||||
import JaasUnsupportedDesktopBrowser from './JaasUnsupportedDesktopBrowser';
|
||||
import UnsupportedDesktopBrowser from './UnsupportedDesktopBrowser';
|
||||
|
||||
const PageSelector = () => {
|
||||
const isJaas = useSelector(isVpaasMeeting);
|
||||
|
||||
if (isJaas) {
|
||||
return <JaasUnsupportedDesktopBrowser />;
|
||||
}
|
||||
|
||||
return <UnsupportedDesktopBrowser />;
|
||||
};
|
||||
|
||||
export default PageSelector;
|
||||
@@ -1,73 +1 @@
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isBrowsersOptimal } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { CHROME, FIREFOX } from './browserLinks';
|
||||
|
||||
/**
|
||||
* The namespace of the CSS styles of UnsupportedDesktopBrowser.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _SNS = 'unsupported-desktop-browser';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link UnsupportedDesktopBrowser}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing unsupported browser page.
|
||||
*
|
||||
* @class UnsupportedDesktopBrowser
|
||||
*/
|
||||
class UnsupportedDesktopBrowser extends Component<Props> {
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = { _SNS }>
|
||||
<h2 className = { `${_SNS}__title` }>
|
||||
It looks like you're using a browser we don't support.
|
||||
</h2>
|
||||
<p className = { `${_SNS}__description` }>
|
||||
Please try again with the latest version of
|
||||
<a
|
||||
className = { `${_SNS}__link` }
|
||||
href = { CHROME } >Chrome</a>
|
||||
{
|
||||
this._showFirefox() && <>or <a
|
||||
className = { `${_SNS}__link` }
|
||||
href = { FIREFOX }>Firefox</a></>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a link to download Firefox is displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_showFirefox() {
|
||||
return isBrowsersOptimal('firefox');
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(UnsupportedDesktopBrowser);
|
||||
export { default } from './DefaultUnsupportedDesktopBrowser';
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as UnsupportedDesktopBrowser }
|
||||
from './UnsupportedDesktopBrowser';
|
||||
export { default as UnsupportedDesktopBrowser } from './PageSelector';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @flow
|
||||
|
||||
import { approveParticipant } from '../../../av-moderation/actions';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconMicrophone } from '../../../base/icons';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
import { isForceMuted } from '../../../participants-pane/functions';
|
||||
|
||||
export type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to
|
||||
* ask to unmute.
|
||||
*/
|
||||
participantID: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which asks the remote participant to unmute.
|
||||
*/
|
||||
class AskUnmuteButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'participantsPane.actions.askUnmute';
|
||||
icon = IconMicrophone;
|
||||
label = 'participantsPane.actions.askUnmute';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and asks the participant to unmute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(approveParticipant(participantID));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { participantID } = ownProps;
|
||||
const participant = getParticipantById(state, participantID);
|
||||
|
||||
return {
|
||||
visible: isLocalParticipantModerator(state)
|
||||
&& (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|
||||
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state))
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(AskUnmuteButton));
|
||||
@@ -1,11 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { ConfirmDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorDialog
|
||||
import AbstractGrantModeratorDialog, { abstractMapStateToProps }
|
||||
from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
@@ -22,11 +23,15 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
contentKey = 'dialog.grantModeratorDialog'
|
||||
onSubmit = { this._onSubmit } />
|
||||
onSubmit = { this._onSubmit }>
|
||||
<Text>
|
||||
{`${this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName })}`}
|
||||
</Text>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(GrantModeratorDialog));
|
||||
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { Text, View, Switch } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { ConfirmDialog } from '../../../base/dialog';
|
||||
@@ -12,6 +13,8 @@ import AbstractMuteEveryoneDialog, {
|
||||
abstractMapStateToProps,
|
||||
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
@@ -28,6 +31,22 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
|
||||
|
||||
/**
|
||||
* Toggles advanced moderation switch.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleModeration() {
|
||||
this.setState(state => {
|
||||
return {
|
||||
audioModerationEnabled: !state.audioModerationEnabled,
|
||||
content: this.props.t(state.audioModerationEnabled
|
||||
? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -39,8 +58,23 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
|
||||
okKey = 'dialog.muteParticipantButton'
|
||||
onSubmit = { this._onSubmit } >
|
||||
<Text style = { this.props._dialogStyles.text }>
|
||||
{ `${this.props.title} \n\n ${this.props.content}` }
|
||||
{ `${this.props.title} \n\n ${this.state.content}` }
|
||||
</Text>
|
||||
{this.props.exclude.length === 0 && <>
|
||||
<Divider style = { styles.dividerWithSpacing } />
|
||||
<View style = { styles.toggleContainer }>
|
||||
<Text
|
||||
style = {{
|
||||
...this.props._dialogStyles.text,
|
||||
...styles.toggleLabel
|
||||
}}>
|
||||
{this.props.t('dialog.moderationAudioLabel')}
|
||||
</Text>
|
||||
<Switch
|
||||
onValueChange = { this._onToggleModeration }
|
||||
value = { !this.state.audioModerationEnabled } />
|
||||
</View>
|
||||
</>}
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { Switch, Text, View } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { ConfirmDialog } from '../../../base/dialog';
|
||||
@@ -12,6 +13,8 @@ import AbstractMuteEveryonesVideoDialog, {
|
||||
abstractMapStateToProps,
|
||||
type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
@@ -24,10 +27,26 @@ type Props = AbstractProps & {
|
||||
* A React Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before muting all remote participants.
|
||||
*
|
||||
* @extends AbstractMuteEveryoneDialog
|
||||
* @extends AbstractMuteEveryonesVideoDialog
|
||||
*/
|
||||
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
|
||||
|
||||
/**
|
||||
* Toggles advanced moderation switch.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleModeration() {
|
||||
this.setState(state => {
|
||||
return {
|
||||
moderationEnabled: !state.moderationEnabled,
|
||||
content: this.props.t(state.moderationEnabled
|
||||
? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -39,8 +58,21 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
|
||||
okKey = 'dialog.muteEveryonesVideoDialogOk'
|
||||
onSubmit = { this._onSubmit } >
|
||||
<Text style = { this.props._dialogStyles.text }>
|
||||
{ `${this.props.title} \n\n ${this.props.content}` }
|
||||
{ `${this.props.title} \n\n ${this.state.content}` }
|
||||
</Text>
|
||||
{this.props.exclude.length === 0 && <>
|
||||
<Divider style = { styles.dividerWithSpacing } />
|
||||
<View style = { styles.toggleContainer }>
|
||||
<Text
|
||||
style = {{ ...this.props._dialogStyles.text,
|
||||
...styles.toggleLabel }}>
|
||||
{this.props.t('dialog.moderationVideoLabel')}
|
||||
</Text>
|
||||
<Switch
|
||||
onValueChange = { this._onToggleModeration }
|
||||
value = { !this.state.moderationEnabled } />
|
||||
</View>
|
||||
</>}
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { PrivateMessageButton } from '../../../chat';
|
||||
import { hideRemoteVideoMenu } from '../../actions.native';
|
||||
import ConnectionStatusButton from '../native/ConnectionStatusButton';
|
||||
|
||||
import AskUnmuteButton from './AskUnmuteButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
@@ -126,6 +127,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
||||
onCancel = { this._onCancel }
|
||||
renderHeader = { this._renderMenuHeader }
|
||||
showSlidingView = { _isParticipantAvailable }>
|
||||
<AskUnmuteButton { ...buttonProps } />
|
||||
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
|
||||
<MuteEveryoneElseButton { ...buttonProps } />
|
||||
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }
|
||||
|
||||
@@ -67,5 +67,24 @@ export default createStyleSheet({
|
||||
|
||||
divider: {
|
||||
backgroundColor: BaseTheme.palette.dividerColor
|
||||
},
|
||||
|
||||
dividerWithSpacing: {
|
||||
backgroundColor: BaseTheme.palette.dividerColor,
|
||||
marginVertical: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
toggleContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
toggleLabel: {
|
||||
marginRight: BaseTheme.spacing[3],
|
||||
maxWidth: '70%'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user