mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-23 20:37:47 +00:00
Compare commits
46 Commits
5337
...
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 | ||
|
|
62e5d6c139 | ||
|
|
06d8956bdb | ||
|
|
adbb5f8ead | ||
|
|
32ed2bccec | ||
|
|
58d98ad7d3 | ||
|
|
e278703c58 | ||
|
|
042a2cb447 | ||
|
|
a8f281a43e | ||
|
|
663752be2c | ||
|
|
380ef3da0b |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ class ReactInstanceManagerHolder {
|
||||
return;
|
||||
}
|
||||
|
||||
SoLoader.init(activity, /* native exopackage */ false);
|
||||
SoLoader.init(activity.getApplication(), /* native exopackage */ false);
|
||||
|
||||
List<ReactPackage> packages
|
||||
= new ArrayList<>(Arrays.asList(
|
||||
|
||||
35
config.js
35
config.js
@@ -45,9 +45,12 @@ var config = {
|
||||
// disabled by the below option.
|
||||
// enableThumbnailReordering: true,
|
||||
|
||||
// Enables XMPP WebSocket (as opposed to BOSH) for the given amount of users.
|
||||
// mobileXmppWsThreshold: 10 // enable XMPP WebSockets on mobile for 10% of the users
|
||||
|
||||
// P2P test mode disables automatic switching to P2P when there are 2
|
||||
// participants in the conference.
|
||||
p2pTestMode: false
|
||||
// p2pTestMode: false,
|
||||
|
||||
// Enables the test specific features consumed by jitsi-meet-torture
|
||||
// testMode: false
|
||||
@@ -71,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,
|
||||
@@ -901,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,
|
||||
|
||||
|
||||
@@ -106,9 +106,7 @@ export async function connect(id, password, roomName) {
|
||||
|
||||
serviceUrl += `?room=${roomName}`;
|
||||
|
||||
// FIXME Remove deprecated 'bosh' option assignment at some point(LJM will be accepting only 'serviceUrl' option
|
||||
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
|
||||
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
|
||||
connectionConfig.serviceUrl = serviceUrl;
|
||||
|
||||
if (connectionConfig.websocketKeepAliveUrl) {
|
||||
connectionConfig.websocketKeepAliveUrl += `?room=${roomName}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
&-circle {
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
.device {
|
||||
&-status {
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
justify-content: center;
|
||||
line-height: 20px;
|
||||
margin-top: 8px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
|
||||
&-error {
|
||||
align-items: flex-start;
|
||||
background-color: #F8AE1A;
|
||||
border-radius: 6px;
|
||||
color: #040404;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@@ -18,14 +27,8 @@
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
margin-right: 10px;
|
||||
width: 16px;
|
||||
|
||||
&--warning {
|
||||
svg path {
|
||||
fill: rgba(241, 173, 51, 1);
|
||||
}
|
||||
}
|
||||
&--ok {
|
||||
svg path {
|
||||
fill: #189b55;
|
||||
|
||||
@@ -3,21 +3,16 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-checkbox-container {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-error {
|
||||
color: white;
|
||||
background-color: #E04757;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: -8px;
|
||||
font-size: 12px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
margin-bottom: 16px;
|
||||
padding: 7px 16px;
|
||||
@@ -128,10 +129,22 @@
|
||||
|
||||
#new-toolbox {
|
||||
bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
transition: none;
|
||||
|
||||
.toolbox-content {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.toolbox-content,
|
||||
.toolbox-content-wrapper,
|
||||
.toolbox-content-items {
|
||||
@@ -163,17 +176,26 @@
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
|
||||
&-controls {
|
||||
input.field {
|
||||
font-size: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.012;
|
||||
margin-bottom: 24px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.con-status {
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.device-status-error {
|
||||
border-radius: 0;
|
||||
margin: 0 -16px;
|
||||
}
|
||||
|
||||
input.field {
|
||||
@@ -183,15 +205,9 @@
|
||||
|
||||
.action-btn {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding: 11px 16px;
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
@@ -227,68 +243,3 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin icon-container($bg, $fill) {
|
||||
.toggle-button-icon-container {
|
||||
background: $bg;
|
||||
|
||||
svg {
|
||||
fill: $fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
transition: background 0.16s ease-out;
|
||||
|
||||
@include flex-centered();
|
||||
|
||||
svg {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.toggle-button-icon-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
position: relative;
|
||||
|
||||
@include flex-centered();
|
||||
}
|
||||
|
||||
&-icon-container {
|
||||
border-radius: 50%;
|
||||
left: -22px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&--toggled {
|
||||
@include icon-container(white, #1C2025);
|
||||
|
||||
&:hover {
|
||||
.toggle-button-icon-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button-icon-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -696,6 +699,7 @@
|
||||
"errorDialOutFailed": "Could not dial out. Call failed",
|
||||
"errorDialOutStatus": "Error getting dial out status",
|
||||
"errorMissingName": "Please enter your name to join the meeting",
|
||||
"errorNoPermissions": "You need to enable microphone and camera access",
|
||||
"errorStatusCode": "Error dialing out, status code: {{status}}",
|
||||
"errorValidation": "Number validation failed",
|
||||
"iWantToDialIn": "I want to dial in",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -181,7 +181,7 @@ export function initAnalytics({ getState }: { getState: Function }, handlers: Ar
|
||||
permanentProperties.appName = getAppName();
|
||||
|
||||
// Report if user is using websocket
|
||||
permanentProperties.websocket = navigator.product !== 'ReactNative' && typeof config.websocket === 'string';
|
||||
permanentProperties.websocket = typeof config.websocket === 'string';
|
||||
|
||||
// Report if user is using the external API
|
||||
permanentProperties.externalApi = typeof API_ID === 'number';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -275,10 +275,17 @@ function _constructOptions(state) {
|
||||
// redux store.
|
||||
const options = _.cloneDeep(state['features/base/config']);
|
||||
|
||||
// Normalize the BOSH URL.
|
||||
let { bosh } = options;
|
||||
let { bosh, websocket } = options;
|
||||
|
||||
if (bosh) {
|
||||
// TESTING: Only enable WebSocket for some percentage of users.
|
||||
if (websocket) {
|
||||
if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
|
||||
websocket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the BOSH URL.
|
||||
if (bosh && !websocket) {
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
if (bosh.startsWith('//')) {
|
||||
@@ -295,14 +302,24 @@ function _constructOptions(state) {
|
||||
// eslint-disable-next-line max-len
|
||||
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Append room to the URL's search.
|
||||
const { room } = state['features/base/conference'];
|
||||
// WebSocket is preferred over BOSH.
|
||||
const serviceUrl = websocket || bosh;
|
||||
|
||||
room && (bosh += `?room=${getBackendSafeRoomName(room)}`);
|
||||
logger.log(`Using service URL ${serviceUrl}`);
|
||||
|
||||
// FIXME Remove deprecated 'bosh' option assignment at some point.
|
||||
options.serviceUrl = options.bosh = bosh;
|
||||
// Append room to the URL's search.
|
||||
const { room } = state['features/base/conference'];
|
||||
|
||||
if (serviceUrl && room) {
|
||||
const roomName = getBackendSafeRoomName(room);
|
||||
|
||||
options.serviceUrl = `${serviceUrl}?room=${roomName}`;
|
||||
|
||||
if (options.websocketKeepAliveUrl) {
|
||||
options.websocketKeepAliveUrl += `?room=${roomName}`;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
type Props = {
|
||||
...DialogProps,
|
||||
type Props = DialogProps & {
|
||||
|
||||
/**
|
||||
* Custom dialog header that replaces the standard heading.
|
||||
@@ -77,6 +76,11 @@ type Props = {
|
||||
*/
|
||||
onDecline?: Function,
|
||||
|
||||
/**
|
||||
* Callback invoked when setting the ref of the Dialog.
|
||||
*/
|
||||
onDialogRef?: Function,
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
@@ -127,7 +131,7 @@ class StatelessDialog extends Component<Props> {
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._renderFooter = this._renderFooter.bind(this);
|
||||
this._setDialogElement = this._setDialogElement.bind(this);
|
||||
this._onDialogRef = this._onDialogRef.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +170,7 @@ class StatelessDialog extends Component<Props> {
|
||||
width = { width || 'medium' }>
|
||||
<div
|
||||
onKeyPress = { this._onKeyPress }
|
||||
ref = { this._setDialogElement }>
|
||||
ref = { this._onDialogRef }>
|
||||
<form
|
||||
className = 'modal-dialog-form'
|
||||
id = 'modal-dialog-form'
|
||||
@@ -319,19 +323,18 @@ class StatelessDialog extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
_setDialogElement: (?HTMLElement) => void;
|
||||
_onDialogRef: (?Element) => void;
|
||||
|
||||
/**
|
||||
* Sets the instance variable for the div containing the component's dialog
|
||||
* element so it can be accessed directly.
|
||||
* Callback invoked when setting the ref of the dialog's child passing the Modal ref.
|
||||
* It is done this way because we cannot directly access the ref of the Modal component.
|
||||
*
|
||||
* @param {HTMLElement} element - The DOM element for the component's
|
||||
* dialog.
|
||||
* @param {HTMLElement} element - The DOM element for the dialog.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setDialogElement(element: ?HTMLElement) {
|
||||
this._dialogElement = element;
|
||||
_onDialogRef(element: ?Element) {
|
||||
this.props.onDialogRef && this.props.onDialogRef(element && element.parentNode);
|
||||
}
|
||||
|
||||
_onKeyPress: (Object) => void;
|
||||
|
||||
3
react/features/base/icons/svg/exclamation-triangle.svg
Normal file
3
react/features/base/icons/svg/exclamation-triangle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2332 14.3254L9.70838 0.910718C9.63451 0.77902 9.52397 0.670633 9.38965 0.598199C8.99846 0.387243 8.50693 0.52716 8.29177 0.910713L0.766813 14.3254C0.701168 14.4424 0.666748 14.5738 0.666748 14.7073C0.666748 15.1451 1.02867 15.4999 1.47512 15.4999H16.5249C16.6611 15.4999 16.7951 15.4662 16.9145 15.4018C17.3057 15.1909 17.4484 14.7089 17.2332 14.3254ZM2.84224 13.9147L9.00002 2.93733L15.1577 13.9147H2.84224ZM8.19177 11.5371C8.19177 11.0993 8.54663 10.7445 8.98437 10.7445H9.01591C9.45365 10.7445 9.80851 11.0993 9.80851 11.5371C9.80851 11.9748 9.45365 12.3297 9.01591 12.3297H8.98437C8.54663 12.3297 8.19177 11.9748 8.19177 11.5371ZM9.00014 6.7815C8.55369 6.7815 8.19177 7.14341 8.19177 7.58986V9.14351C8.19177 9.58996 8.55369 9.95188 9.00014 9.95188C9.44659 9.95188 9.80851 9.58996 9.80851 9.14351V7.58986C9.80851 7.14341 9.44659 6.7815 9.00014 6.7815Z" fill="#040404"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -51,6 +51,7 @@ export { default as IconEmail } from './envelope.svg';
|
||||
export { default as IconEventNote } from './event_note.svg';
|
||||
export { default as IconExclamation } from './exclamation.svg';
|
||||
export { default as IconExclamationSolid } from './exclamation-solid.svg';
|
||||
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
|
||||
export { default as IconExitFullScreen } from './exit-full-screen.svg';
|
||||
export { default as IconFeedback } from './feedback.svg';
|
||||
export { default as IconFullScreen } from './full-screen.svg';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Icon, IconCheck } from '../../../icons';
|
||||
|
||||
const mainClass = 'toggle-button';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* If the button is toggled or not.
|
||||
*/
|
||||
isToggled?: boolean,
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Button used as a toggle.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ToggleButton({ children, isToggled, onClick }: Props) {
|
||||
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
if (onClick && (e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}, [ onClick ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-checked = { isToggled }
|
||||
className = { className }
|
||||
id = 'toggle-button'
|
||||
onClick = { onClick }
|
||||
onKeyPress = { onKeyPressHandler }
|
||||
role = 'switch'
|
||||
tabIndex = { 0 }>
|
||||
<div className = 'toggle-button-container'>
|
||||
<div className = 'toggle-button-icon-container'>
|
||||
<Icon
|
||||
className = 'toggle-button-icon'
|
||||
size = { 10 }
|
||||
src = { IconCheck } />
|
||||
</div>
|
||||
<label htmlFor = 'toggle-button'>{children}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToggleButton;
|
||||
@@ -3,4 +3,3 @@
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as PreMeetingScreen } from './PreMeetingScreen';
|
||||
export { default as ToggleButton } from './ToggleButton';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -258,6 +258,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ const SCORES = [
|
||||
'feedback.veryGood'
|
||||
];
|
||||
|
||||
type Scrollable = {
|
||||
scroll: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FeedbackDialog}.
|
||||
*/
|
||||
@@ -171,6 +175,12 @@ class FeedbackDialog extends Component<Props, State> {
|
||||
this._onScoreContainerMouseLeave
|
||||
= this._onScoreContainerMouseLeave.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
|
||||
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
|
||||
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
|
||||
this._onScrollTop = (node: ?Scrollable) => {
|
||||
node && node.scroll && node.scroll(0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,6 +254,7 @@ class FeedbackDialog extends Component<Props, State> {
|
||||
<Dialog
|
||||
okKey = 'dialog.Submit'
|
||||
onCancel = { this._onCancel }
|
||||
onDialogRef = { this._onScrollTop }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = 'feedback-dialog'>
|
||||
@@ -364,6 +375,8 @@ class FeedbackDialog extends Component<Props, State> {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onScrollTop: (node: ?Scrollable) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ import ThumbnailWrapper from './ThumbnailWrapper';
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Fixes case in which context menu overflows and creates a scroll on the whole filmstrip videos pane.
|
||||
*/
|
||||
const TILEVIEW_VIDEO_PANES_STYLE = { overflow: 'visible' };
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Filmstrip}.
|
||||
*/
|
||||
@@ -69,6 +64,11 @@ type Props = {
|
||||
*/
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether this is a recorder or not.
|
||||
*/
|
||||
_iAmRecorder: boolean,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip button is enabled.
|
||||
*/
|
||||
@@ -240,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;
|
||||
}
|
||||
@@ -299,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';
|
||||
}
|
||||
|
||||
@@ -391,7 +397,6 @@ class Filmstrip extends PureComponent <Props> {
|
||||
overscanRowCount = { 1 }
|
||||
rowCount = { _rows }
|
||||
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
|
||||
style = { TILEVIEW_VIDEO_PANES_STYLE }
|
||||
width = { _filmstripWidth }>
|
||||
{
|
||||
ThumbnailWrapper
|
||||
@@ -534,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;
|
||||
@@ -602,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.
|
||||
|
||||
@@ -29,7 +29,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
store.dispatch(setTileViewDimensions());
|
||||
break;
|
||||
case PARTICIPANT_JOINED: {
|
||||
updateRemoteParticipants(store);
|
||||
updateRemoteParticipants(store, action.participant?.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { Component } from 'react';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { isIosMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import {
|
||||
IconCloseCircle,
|
||||
@@ -395,6 +396,11 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showVolumeSlider = !isIosMobileBrowser()
|
||||
&& overflowDrawer
|
||||
&& typeof _volume === 'number'
|
||||
&& !isNaN(_volume);
|
||||
|
||||
const actions
|
||||
= _participant?.isFakeParticipant ? (
|
||||
<>
|
||||
@@ -463,7 +469,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
{ overflowDrawer && typeof _volume === 'number' && !isNaN(_volume)
|
||||
{ showVolumeSlider
|
||||
&& <ContextMenuItemGroup>
|
||||
<VolumeSlider
|
||||
initialValue = { _volume }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,33 +7,26 @@ import { getRoomName } from '../../base/conference';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Icon, IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
|
||||
import { isVideoMutedByUser } from '../../base/media';
|
||||
import { ActionButton, InputField, PreMeetingScreen, ToggleButton } from '../../base/premeeting';
|
||||
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
|
||||
import { connect } from '../../base/redux';
|
||||
import { getDisplayName, updateSettings } from '../../base/settings';
|
||||
import { getLocalJitsiVideoTrack } from '../../base/tracks';
|
||||
import {
|
||||
joinConference as joinConferenceAction,
|
||||
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
||||
setSkipPrejoin as setSkipPrejoinAction,
|
||||
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
|
||||
} from '../actions';
|
||||
import {
|
||||
isDeviceStatusVisible,
|
||||
isDisplayNameRequired,
|
||||
isJoinByPhoneButtonVisible,
|
||||
isJoinByPhoneDialogVisible,
|
||||
isPrejoinSkipped
|
||||
isJoinByPhoneDialogVisible
|
||||
} from '../functions';
|
||||
|
||||
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the 'skip prejoin' button is toggled or not.
|
||||
*/
|
||||
buttonIsToggled: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
@@ -69,11 +62,6 @@ type Props = {
|
||||
*/
|
||||
roomName: string,
|
||||
|
||||
/**
|
||||
* Sets visibility of the prejoin page for the next sessions.
|
||||
*/
|
||||
setSkipPrejoin: Function,
|
||||
|
||||
/**
|
||||
* Sets visibility of the 'JoinByPhoneDialog'.
|
||||
*/
|
||||
@@ -138,7 +126,6 @@ class Prejoin extends Component<Props, State> {
|
||||
this._closeDialog = this._closeDialog.bind(this);
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
|
||||
this._onToggleButtonClick = this._onToggleButtonClick.bind(this);
|
||||
this._onDropdownClose = this._onDropdownClose.bind(this);
|
||||
this._onOptionsClick = this._onOptionsClick.bind(this);
|
||||
this._setName = this._setName.bind(this);
|
||||
@@ -183,18 +170,6 @@ class Prejoin extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_onToggleButtonClick: () => void;
|
||||
|
||||
/**
|
||||
* Handler for the toggle button.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleButtonClick() {
|
||||
this.props.setSkipPrejoin(!this.props.buttonIsToggled);
|
||||
}
|
||||
|
||||
_onDropdownClose: () => void;
|
||||
|
||||
/**
|
||||
@@ -321,7 +296,6 @@ class Prejoin extends Component<Props, State> {
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
showDeviceStatus = { deviceStatusVisible }
|
||||
skipPrejoinButton = { this._renderSkipPrejoinButton() }
|
||||
title = { t('prejoin.joinMeeting') }
|
||||
videoMuted = { !showCameraPreview }
|
||||
videoTrack = { videoTrack }>
|
||||
@@ -400,25 +374,6 @@ class Prejoin extends Component<Props, State> {
|
||||
</PreMeetingScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the 'skip prejoin' button.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderSkipPrejoinButton() {
|
||||
const { buttonIsToggled, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-checkbox-container'>
|
||||
<ToggleButton
|
||||
isToggled = { buttonIsToggled }
|
||||
onClick = { this._onToggleButtonClick }>
|
||||
{t('prejoin.doNotShow')}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,7 +387,6 @@ function mapStateToProps(state): Object {
|
||||
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
|
||||
|
||||
return {
|
||||
buttonIsToggled: isPrejoinSkipped(state),
|
||||
name,
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
roomName: getRoomName(state),
|
||||
@@ -448,7 +402,6 @@ const mapDispatchToProps = {
|
||||
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
||||
joinConference: joinConferenceAction,
|
||||
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
|
||||
setSkipPrejoin: setSkipPrejoinAction,
|
||||
updateSettings
|
||||
};
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconCheckSolid, IconExclamation } from '../../../base/icons';
|
||||
import { Icon, IconCheckSolid, IconExclamationTriangle } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
getDeviceStatusType,
|
||||
getDeviceStatusText,
|
||||
getRawError
|
||||
getDeviceStatusText
|
||||
} from '../../functions';
|
||||
|
||||
export type Props = {
|
||||
@@ -24,11 +23,6 @@ export type Props = {
|
||||
*/
|
||||
deviceStatusType: string,
|
||||
|
||||
/**
|
||||
* The error coming from device configuration.
|
||||
*/
|
||||
rawError: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
@@ -37,7 +31,7 @@ export type Props = {
|
||||
|
||||
const iconMap = {
|
||||
warning: {
|
||||
src: IconExclamation,
|
||||
src: IconExclamationTriangle,
|
||||
className: 'device-icon--warning'
|
||||
},
|
||||
ok: {
|
||||
@@ -52,25 +46,23 @@ const iconMap = {
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props) {
|
||||
function DeviceStatus({ deviceStatusType, deviceStatusText, t }: Props) {
|
||||
const { src, className } = iconMap[deviceStatusType];
|
||||
const hasError = deviceStatusType === 'warning';
|
||||
const containerClassName = `device-status ${hasError ? 'device-status-error' : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'device-status'
|
||||
className = { containerClassName }
|
||||
role = 'alert'
|
||||
tabIndex = { -1 }>
|
||||
<Icon
|
||||
className = { `device-icon ${className}` }
|
||||
size = { 16 }
|
||||
src = { src } />
|
||||
<span
|
||||
role = 'heading'>
|
||||
{t(deviceStatusText)}
|
||||
<span role = 'heading'>
|
||||
{hasError ? t('prejoin.errorNoPermissions') : t(deviceStatusText)}
|
||||
</span>
|
||||
{ rawError && <span>
|
||||
{ rawError }
|
||||
</span> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -84,8 +76,7 @@ function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
deviceStatusText: getDeviceStatusText(state),
|
||||
deviceStatusType: getDeviceStatusType(state),
|
||||
rawError: getRawError(state)
|
||||
deviceStatusType: getDeviceStatusType(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user