Compare commits

..

36 Commits

Author SHA1 Message Date
titus.moldovan
63754f01a5 chore(deps) updates react-native-webrtc 2021-10-15 12:58:12 +03:00
Zoltán Ulrich
dcf677bdf0 fix(ios) Fixes RCTBridge not being released when JitsiMeet is trying to destroy its bridge on some OS versions 2021-10-14 13:05:37 +02:00
titus.moldovan
eaea2cdef0 chore(versions) bump iOS SDK version 2021-10-08 12:29:41 +03:00
Saúl Ibarra Corretgé
bcad87e894 chore(versions) bump iOS app version 2021-10-07 11:47:22 +02:00
Saúl Ibarra Corretgé
d500493649 chore(deps) update lib-jitsi-meet to fix iOS 15 issue
Not unmuting when "everyone starts muted".
2021-10-07 11:45:53 +02:00
tmoldovan8x8
8ad3ef0022 chore(versions) bump Android SDK version 2021-10-05 09:24:39 +03:00
Saúl Ibarra Corretgé
d0664d9ea0 chore(versions) bump iOS SDK version 2021-10-01 15:49:11 +02:00
Saúl Ibarra Corretgé
45c40d7afc chore(versions) bump iOS app version 2021-10-01 15:48:09 +02:00
Saúl Ibarra Corretgé
e33edfd2a5 fix(rn,conference) fix unmute when "everyone starts muted" is set
Since iOS 15 we really need the audio stream to exist, so make sure we don't
destroy it even when "everyone starts muted" is set, we'll just mute it.

Fixes: https://github.com/jitsi/jitsi-meet/issues/10053
2021-10-01 15:47:22 +02:00
Saúl Ibarra Corretgé
96b2a07172 chore(versions) bump iOS app version 2021-09-30 15:18:26 +02:00
Saúl Ibarra Corretgé
0e8c01af75 fix(ios) fix crash on WebSocket errors
Bacckport
748aa13747

Fixes:

~~~
Fatal Exception: NSInvalidArgumentException
0  CoreFoundation                 0x129708 __exceptionPreprocess
1  libobjc.A.dylib                0x287a8 objc_exception_throw
2  CoreFoundation                 0x19b9c8 -[__NSCFString characterAtIndex:].cold.1
3  CoreFoundation                 0x1a7a20 -[__NSPlaceholderDictionary initWithCapacity:].cold.1
4  CoreFoundation                 0x164c0 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]
5  CoreFoundation                 0x8de0 +[NSDictionary dictionaryWithObjects:forKeys:count:]
6  JitsiMeetSDK                   0x5704b4 -[RCTWebSocketModule webSocket:didFailWithError:] + 168 (RCTWebSocketModule.m:168)
7  JitsiMeetSDK                   0x54a5ec __33-[RCTSRWebSocket _failWithError:]_block_invoke_2 + 622 (RCTSRWebSocket.m:622)
8  libdispatch.dylib              0x2a84 _dispatch_call_block_and_release
9  libdispatch.dylib              0x481c _dispatch_client_callout
10 libdispatch.dylib              0xc004 _dispatch_lane_serial_drain
11 libdispatch.dylib              0xcc00 _dispatch_lane_invoke
12 libdispatch.dylib              0x174bc _dispatch_workloop_worker_thread
13 libsystem_pthread.dylib        0x37a4 _pthread_wqthread
14 libsystem_pthread.dylib        0xa74c start_wqthread
~~~
2021-09-30 15:12:46 +02:00
tmoldovan8x8
3049ad6dbf feat(android) target sdk 31 2021-09-30 14:06:37 +02:00
Saúl Ibarra Corretgé
9a30ad87a7 chore(versions) bump Android sdk version 2021-09-29 14:49:03 +02:00
Saúl Ibarra Corretgé
11c05560c4 chore(versions) bump Android app version 2021-09-29 14:15:43 +02:00
titus.moldovan
d92fa59c74 fix(android) disables uncompressed native libs usage 2021-09-29 14:15:11 +02:00
Saúl Ibarra Corretgé
4eb1d16faa chore(deps): update react-native-webrtc@1.92.2
Fixes a crash on Android
2021-09-29 14:13:47 +02:00
Saúl Ibarra Corretgé
91adc70724 chore(rn,versions) bump sdk version 2021-09-24 11:09:25 +02:00
Saúl Ibarra Corretgé
15b083c182 fix(ios) create audio track early
In iOS 15 we observe that not creating the audio track early may result in not
getting audio after unmuting for the first time.

Creating the audio track early means the first unmute doesn't need to add the
track to the conference, resulting in a much faster operation.

Note that creating the track early doesn't mean we will start unmuted, the track
will be muted.
2021-09-24 11:01:33 +02:00
titus.moldovan
26fdaf7b8d fix(rn) add default value when retrieving e2ee redux state 2021-09-24 10:11:52 +02:00
Saúl Ibarra Corretgé
64a9874d63 chore(rn,versions) bump sdk version 2021-09-23 14:13:51 +02:00
Saúl Ibarra Corretgé
753cffc458 fix(ios) fix black screen when using Bluetooth in iOS 15
Fixes: https://github.com/jitsi/jitsi-meet/issues/9996

On iOS 15 Bluetooth devices are reported twice for some reason, one with the
normal type "Bluetooth" but another type without a know (to me) type, and the
uid ends in "-reference".

While we send those unkwno devices to JS, we were not filtering them properly.
This patch skips them altogether.
2021-09-23 14:11:49 +02:00
Saúl Ibarra Corretgé
99e56e229d chore(rn,versions) bump sdk version 2021-09-22 16:13:57 +02:00
robertpin
c3dae1f6e9 feat(rn,av-moderation) updated advanced moderation on Native
Updated participants list to:
- show Moderator label
- show correct status icons (red for force muted)
- show participants in the right order

Updated moderation to:
- show moderation menu at all times
- make moderation options functional

Updated notifications:
- fixed raise hand to show name
- display moderator rights granted

Updated mute/ stop video for all dialogs to include moderation toggles

Added ask to unmute button

Fix comments on ask to unmute

Co-authored-by: robertpin <robert.pin9@gmail.com>
2021-09-22 16:05:42 +02:00
robertpin
703e43ecd7 fix(av-moderation) Display Allow Video instead of Ask to Unmute (#9991)
When the user is allowed audio but not video display Allow Video button instead of Ask to Unmute
2021-09-22 16:26:55 +03:00
Vlad Piersec
0bf52b613b feat(api): Expose event used for sending browser support 2021-09-22 09:20:56 +03:00
robertpin
584ec7c82e fix(reactions) Reactions improvements (#9964)
* Register shortcuts on mount

* Changed icon for reactions menu

* Enable reactions by default

* Fix unreadCount bug

When having unread messages and sending a reaction the unread count now shows the correct count

* Fix overflow menu bottom color when reactions are enabled

* Revert raise hand icon

* Update raise hand functionality

On desktop show raise button with arrow for reactions. Only show raise hand in the reactions menu on mobile

* Fix lint error

Add required prop to ToolboxButtonWithIcon

* Legacy support for enable reactions

If disableReactions is undefined treat it as true

* Remove unnecessary code

* Fix unread counter showing negative count

* Fix unreadCount with reactions

UnreadCount ignores all reactions messages

* Fixed typo

* Fix background color
2021-09-21 12:30:24 -05:00
robertpin
5f5cac0e01 fix(config,notifications) fix rendering moderator notifications (#9986)
Move DISABLE_FOCUS_INDICATOR from interface_config.js to config.js (disableModeratorIndicator).
2021-09-21 17:38:06 +02:00
Vlad Piersec
0a9b9bb41d fix(responsive-ui): Shrink self view when in portrait mode on mobile web 2021-09-21 16:47:47 +03:00
tmoldovan8x8
5ad98d193a feat(e2ee) disable e2ee when large number of participants 2021-09-21 14:00:23 +03:00
Дамян Минков
4cd5be9d8b chore(deps) lib-jitsi-meet@latest
* fix(JitsiConference) Check for room before calling isFocus method on the room object.
* fix(Jingle) Reverse the order of ssrcs signaled for Firefox. This fixes an issue where the bridge doesn't forward the HD stream from Firefox to other users in the call. The order of the ssrcs produced by the browser is from Highest resolution to lowest whereas the bridge assumes it to be from lowest to highest as is the case in Chrome and Safari.
* fix(codec-selection): Impose VP9 bitrates only when VP9 is the negotiated codec. If Jicofo doesn't offer VP9 but the client expresses a preference for VP9, VP9 bitrates were being imposed before.

609e3d5a1a...3b8baa9d3b
2021-09-20 17:35:08 -05:00
Horatiu Muresan
5514be630d feat(conference-info-header) Make conference info header configurable. (#9638) 2021-09-20 21:12:56 +03:00
hmuresan
6a6146727f fix(notifications) Fix recording start notif not disappearing 2021-09-20 19:08:12 +03:00
Avram Tudor
52e9e90b3a fix(jaas) do not show overriden unsupported browser page for jaas users (#9962) 2021-09-20 11:38:46 +03:00
Alexey Matveev
7ff3b669ee fix(lang) update Russian translation
Co-authored-by: Alexey Matveev <malex@1forma.ru>
2021-09-18 11:15:13 +02:00
Hristo Terezov
e791c4f70c fix(recorder): tile view
In the case of the recorder we were not taking into account that the
local thumbnail is not visible. This was braking the rendering  and
positioning of the thumbnails in tile view.
2021-09-17 18:20:00 -05:00
Дамян Минков
fc75fd9644 chore(deps) lib-jitsi-meet@latest
* fix: Reads shard name and from disco-info if available.
* chore(deps): bump sdp-interop to get another fix for ICE restart
* update sdp-interop to include Unified ICE restart fix

fbf85bdcec...609e3d5a1a
2021-09-17 15:51:23 -05:00
95 changed files with 1328 additions and 627 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -74,8 +74,11 @@ var config = {
// callStatsThreshold: 5 // enable callstats for 5% of the users.
},
// Enables reactions feature.
// enableReactions: false,
// Disables moderator indicators.
// disableModeratorIndicator: false,
// Disables the reactions feature.
// disableReactions: true,
// Disables polls feature.
// disablePolls: false,
@@ -904,15 +907,32 @@ var config = {
// If true, tile view will not be enabled automatically when the participants count threshold is reached.
// disableTileView: true,
// Controls the visibility and behavior of the top header conference info labels.
// If a label's id is not in any of the 2 arrays, it will not be visible at all on the header.
// conferenceInfo: {
// // those labels will not be hidden in tandem with the toolbox.
// alwaysVisible: ['recording', 'local-recording'],
// // those labels will be auto-hidden in tandem with the toolbox buttons.
// autoHide: [
// 'subject',
// 'conference-timer',
// 'participants-count',
// 'e2ee',
// 'transcribing',
// 'video-quality',
// 'insecure-room'
// ]
// },
// Hides the conference subject
// hideConferenceSubject: true,
// Hides the recording label
// hideRecordingLabel: false,
// Hides the conference timer.
// hideConferenceTimer: true,
// Hides the recording label
// hideRecordingLabel: false,
// Hides the participants stats
// hideParticipantsStats: true,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -127,6 +127,7 @@
}
- (void)destroyReactNativeBridge {
[_bridgeWrapper invalidate];
_bridgeWrapper = nil;
}

View File

@@ -34,4 +34,6 @@
@property (nonatomic, readonly, strong) RCTBridge *bridge;
- (void)invalidate;
@end

View File

@@ -33,6 +33,10 @@
return self;
}
- (void)invalidate {
[_bridge invalidate];
}
#pragma mark helper methods for getting the packager URL
#if DEBUG

View File

@@ -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": "Редактировать профиль",

View File

@@ -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",

View File

@@ -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.
*

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -1,6 +1,7 @@
// @flow
import '../analytics/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware';
import '../base/config/middleware';
import '../base/jwt/middleware';

View File

@@ -1,7 +1,6 @@
// @flow
import '../authentication/middleware';
import '../av-moderation/middleware';
import '../base/devices/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

@@ -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';

View File

@@ -1,6 +1,5 @@
// @flow
import '../av-moderation/reducer';
import '../base/devices/reducer';
import '../e2ee/reducer';
import '../feedback/reducer';

View File

@@ -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);
}
};

View File

@@ -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));
}
}

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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() }

View File

@@ -213,6 +213,13 @@ ColorSchemeRegistry.register('BottomSheet', {
*/
sheetHeader: {
backgroundColor: BaseTheme.palette.ui02
},
/**
* Bottom sheet's background color with footer.
*/
sheetFooter: {
backgroundColor: BaseTheme.palette.bottomSheet
}
});

View File

@@ -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));

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;
}
/**

View File

@@ -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') {

View File

@@ -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,

View 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'
]
};

View 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;
};

View File

@@ -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)
};
}

View File

@@ -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>
);

View File

@@ -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>
);
}

View 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);

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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)
};
}

View File

@@ -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'
};

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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>);
}

View File

@@ -64,6 +64,11 @@ type Props = {
*/
_filmstripHeight: number,
/**
* Whether this is a recorder or not.
*/
_iAmRecorder: boolean,
/**
* Whether the filmstrip button is enabled.
*/
@@ -235,14 +240,14 @@ class Filmstrip extends PureComponent <Props> {
* @returns {Object}
*/
_calculateIndices(startIndex, stopIndex) {
const { _currentLayout, _thumbnailsReordered } = this.props;
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
let start = startIndex;
let stop = stopIndex;
if (_thumbnailsReordered) {
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
// endpoint. The remote participants start from index 1.
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
start = Math.max(startIndex - 1, 0);
stop = stopIndex - 1;
}
@@ -294,18 +299,24 @@ class Filmstrip extends PureComponent <Props> {
* @returns {string} - The key.
*/
_gridItemKey({ columnIndex, rowIndex }) {
const { _columns, _remoteParticipants, _remoteParticipantsLength, _thumbnailsReordered } = this.props;
const {
_columns,
_iAmRecorder,
_remoteParticipants,
_remoteParticipantsLength,
_thumbnailsReordered
} = this.props;
const index = (rowIndex * _columns) + columnIndex;
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
const remoteIndex = _thumbnailsReordered ? index - 1 : index;
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
if (index > _remoteParticipantsLength) {
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
return `empty-${index}`;
}
if (index === localIndex) {
if (!_iAmRecorder && index === localIndex) {
return 'local';
}
@@ -528,7 +539,7 @@ class Filmstrip extends PureComponent <Props> {
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { testing = {} } = state['features/base/config'];
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
@@ -596,6 +607,7 @@ function _mapStateToProps(state) {
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,

View File

@@ -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
};

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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
};
}

View File

@@ -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) {

View File

@@ -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.
*/

View File

@@ -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 }

View File

@@ -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) {

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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
};
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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,

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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
};
}

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -204,7 +204,7 @@ export function showStartedRecordingNotification(
}
}
dispatch(showNotification(dialogProps));
dispatch(showNotification(dialogProps, NOTIFICATION_TIMEOUT));
};
}

View File

@@ -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)
};
}

View File

@@ -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.
*/

View File

@@ -10,6 +10,7 @@ import {
import { toState } from '../base/redux';
import { parseStandardURIString } from '../base/util';
import { isFollowMeActive } from '../follow-me';
import { isReactionsEnabled } from '../reactions/functions.any';
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
@@ -174,7 +175,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsTalkWhileMuted,
soundsReactions
} = state['features/base/settings'];
const { enableReactions } = state['features/base/config'];
const enableReactions = isReactionsEnabled(state);
return {
soundsIncomingMessage,

View File

@@ -1,84 +0,0 @@
// @flow
import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
type Props = AbstractButtonProps & {
/**
* Whether or not the local participant's hand is raised.
*/
_raisedHand: boolean,
};
/**
* Implementation of a button for toggling raise hand functionality.
*/
class RaiseHandButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
icon = IconRaisedHand
label = 'toolbar.raiseYourHand';
toggledLabel = 'toolbar.lowerYourHand'
/**
* Retrieves tooltip dynamically.
*/
get tooltip() {
return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand';
}
/**
* Required by linter due to AbstractButton overwritten prop being writable.
*
* @param {string} value - The value.
*/
set tooltip(value) {
return value;
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._raisedHand;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = state => {
const localParticipant = getLocalParticipant(state);
return {
_raisedHand: localParticipant.raisedHand
};
};
export default translate(connect(mapStateToProps)(RaiseHandButton));

View File

@@ -17,7 +17,6 @@ import { translate } from '../../../base/i18n';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipantCount,
haveParticipantWithScreenSharingFeature,
raiseHand
} from '../../../base/participants';
@@ -87,7 +86,6 @@ import AudioSettingsButton from './AudioSettingsButton';
import FullscreenButton from './FullscreenButton';
import OverflowMenuButton from './OverflowMenuButton';
import ProfileButton from './ProfileButton';
import RaiseHandButton from './RaiseHandButton';
import Separator from './Separator';
import ShareDesktopButton from './ShareDesktopButton';
import ToggleCameraButton from './ToggleCameraButton';
@@ -180,11 +178,6 @@ type Props = {
*/
_overflowMenuVisible: boolean,
/**
* Number of participants in the conference.
*/
_participantCount: number,
/**
* Whether or not the participants pane is open.
*/
@@ -254,16 +247,12 @@ type Props = {
declare var APP: Object;
type State = {
reactionsShortcutsRegistered: boolean
};
/**
* Implements the conference toolbox on React/Web.
*
* @extends Component
*/
class Toolbox extends Component<Props, State> {
class Toolbox extends Component<Props> {
/**
* Initializes a new {@code Toolbox} instance.
*
@@ -273,10 +262,6 @@ class Toolbox extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
reactionsShortcutsRegistered: false
};
// Bind event handlers so they are only bound once per instance.
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
@@ -306,7 +291,7 @@ class Toolbox extends Component<Props, State> {
* @returns {void}
*/
componentDidMount() {
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _participantCount } = this.props;
const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
const KEYBOARD_SHORTCUTS = [
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
character: 'A',
@@ -355,7 +340,7 @@ class Toolbox extends Component<Props, State> {
}
});
if (_reactionsEnabled && _participantCount > 1) {
if (_reactionsEnabled) {
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key));
@@ -389,7 +374,7 @@ class Toolbox extends Component<Props, State> {
* @inheritdoc
*/
componentDidUpdate(prevProps) {
const { _dialog, _reactionsEnabled, _participantCount, dispatch, t } = this.props;
const { _dialog, dispatch } = this.props;
if (prevProps._overflowMenuVisible
@@ -398,41 +383,6 @@ class Toolbox extends Component<Props, State> {
this._onSetOverflowVisible(false);
dispatch(setToolbarHovered(false));
}
if (!this.state.reactionsShortcutsRegistered
&& (prevProps._reactionsEnabled !== _reactionsEnabled
|| prevProps._participantCount !== _participantCount)) {
if (_reactionsEnabled && _participantCount > 1) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
reactionsShortcutsRegistered: true
});
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key));
sendAnalytics(createShortcutEvent(
`reaction.${key}`
));
};
return {
character: REACTIONS[key].shortcutChar,
exec: onShortcutSendReaction,
helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
altKey: true
};
});
REACTION_SHORTCUTS.forEach(shortcut => {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription,
shortcut.altKey);
});
}
}
}
/**
@@ -445,7 +395,7 @@ class Toolbox extends Component<Props, State> {
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter));
if (this.props._reactionsEnabled && this.state.reactionsShortcutsRegistered) {
if (this.props._reactionsEnabled) {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter, true));
@@ -613,8 +563,7 @@ class Toolbox extends Component<Props, State> {
const {
_feedbackConfigured,
_isMobile,
_screenSharing,
_reactionsEnabled
_screenSharing
} = this.props;
const microphone = {
@@ -651,8 +600,8 @@ class Toolbox extends Component<Props, State> {
const raisehand = {
key: 'raisehand',
Content: _reactionsEnabled ? ReactionsMenuButton : RaiseHandButton,
handleClick: _reactionsEnabled ? null : this._onToolbarToggleRaiseHand,
Content: ReactionsMenuButton,
handleClick: this._onToolbarToggleRaiseHand,
group: 2
};
@@ -1388,7 +1337,6 @@ function _mapStateToProps(state, ownProps) {
_localParticipantID: localParticipant?.id,
_localVideo: localVideo,
_overflowMenuVisible: overflowMenuVisible,
_participantCount: getParticipantCount(state),
_participantsPaneOpen: getParticipantsPaneOpen(state),
_raisedHand: localParticipant?.raisedHand,
_reactionsEnabled: isReactionsEnabled(state),

View File

@@ -0,0 +1,73 @@
/* @flow */
import React, { Component } from 'react';
import { isBrowsersOptimal } from '../../base/environment';
import { translate } from '../../base/i18n';
import { CHROME, FIREFOX } from './browserLinks';
/**
* The namespace of the CSS styles of UnsupportedDesktopBrowser.
*
* @private
* @type {string}
*/
const _SNS = 'unsupported-desktop-browser';
/**
* The type of the React {@code Component} props of
* {@link JaasUnsupportedDesktopBrowser}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* React component representing unsupported browser page.
*
* @class UnsupportedDesktopBrowser
*/
class JaasUnsupportedDesktopBrowser extends Component<Props> {
/**
* Renders the component.
*
* @returns {ReactElement}
*/
render() {
return (
<div className = { _SNS }>
<h2 className = { `${_SNS}__title` }>
It looks like you're using a browser we don't support.
</h2>
<p className = { `${_SNS}__description` }>
Please try again with the latest version of&nbsp;
<a
className = { `${_SNS}__link` }
href = { CHROME } >Chrome</a>&nbsp;
{
this._showFirefox() && <>or <a
className = { `${_SNS}__link` }
href = { FIREFOX }>Firefox</a></>
}
</p>
</div>
);
}
/**
* Returns whether or not a link to download Firefox is displayed.
*
* @private
* @returns {boolean}
*/
_showFirefox() {
return isBrowsersOptimal('firefox');
}
}
export default translate(JaasUnsupportedDesktopBrowser);

View File

@@ -0,0 +1 @@
export { default } from './DefaultUnsupportedDesktopBrowser';

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { isVpaasMeeting } from '../../jaas/functions';
import JaasUnsupportedDesktopBrowser from './JaasUnsupportedDesktopBrowser';
import UnsupportedDesktopBrowser from './UnsupportedDesktopBrowser';
const PageSelector = () => {
const isJaas = useSelector(isVpaasMeeting);
if (isJaas) {
return <JaasUnsupportedDesktopBrowser />;
}
return <UnsupportedDesktopBrowser />;
};
export default PageSelector;

View File

@@ -1,73 +1 @@
/* @flow */
import React, { Component } from 'react';
import { isBrowsersOptimal } from '../../base/environment';
import { translate } from '../../base/i18n';
import { CHROME, FIREFOX } from './browserLinks';
/**
* The namespace of the CSS styles of UnsupportedDesktopBrowser.
*
* @private
* @type {string}
*/
const _SNS = 'unsupported-desktop-browser';
/**
* The type of the React {@code Component} props of
* {@link UnsupportedDesktopBrowser}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* React component representing unsupported browser page.
*
* @class UnsupportedDesktopBrowser
*/
class UnsupportedDesktopBrowser extends Component<Props> {
/**
* Renders the component.
*
* @returns {ReactElement}
*/
render() {
return (
<div className = { _SNS }>
<h2 className = { `${_SNS}__title` }>
It looks like you're using a browser we don't support.
</h2>
<p className = { `${_SNS}__description` }>
Please try again with the latest version of&nbsp;
<a
className = { `${_SNS}__link` }
href = { CHROME } >Chrome</a>&nbsp;
{
this._showFirefox() && <>or <a
className = { `${_SNS}__link` }
href = { FIREFOX }>Firefox</a></>
}
</p>
</div>
);
}
/**
* Returns whether or not a link to download Firefox is displayed.
*
* @private
* @returns {boolean}
*/
_showFirefox() {
return isBrowsersOptimal('firefox');
}
}
export default translate(UnsupportedDesktopBrowser);
export { default } from './DefaultUnsupportedDesktopBrowser';

View File

@@ -1,2 +1 @@
export { default as UnsupportedDesktopBrowser }
from './UnsupportedDesktopBrowser';
export { default as UnsupportedDesktopBrowser } from './PageSelector';

View File

@@ -0,0 +1,65 @@
// @flow
import { approveParticipant } from '../../../av-moderation/actions';
import { translate } from '../../../base/i18n';
import { IconMicrophone } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isForceMuted } from '../../../participants-pane/functions';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to
* ask to unmute.
*/
participantID: string,
};
/**
* An abstract remote video menu button which asks the remote participant to unmute.
*/
class AskUnmuteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'participantsPane.actions.askUnmute';
icon = IconMicrophone;
label = 'participantsPane.actions.askUnmute';
/**
* Handles clicking / pressing the button, and asks the participant to unmute.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(approveParticipant(participantID));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {Props}
*/
function mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const participant = getParticipantById(state, participantID);
return {
visible: isLocalParticipantModerator(state)
&& (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state))
};
}
export default translate(connect(mapStateToProps)(AskUnmuteButton));

View File

@@ -1,11 +1,12 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorDialog
import AbstractGrantModeratorDialog, { abstractMapStateToProps }
from '../AbstractGrantModeratorDialog';
/**
@@ -22,11 +23,15 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
return (
<ConfirmDialog
contentKey = 'dialog.grantModeratorDialog'
onSubmit = { this._onSubmit } />
onSubmit = { this._onSubmit }>
<Text>
{`${this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName })}`}
</Text>
</ConfirmDialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(GrantModeratorDialog));
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@@ -1,7 +1,8 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { Text, View, Switch } from 'react-native';
import { Divider } from 'react-native-paper';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
@@ -12,6 +13,8 @@ import AbstractMuteEveryoneDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
import styles from './styles';
type Props = AbstractProps & {
/**
@@ -28,6 +31,22 @@ type Props = AbstractProps & {
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Toggles advanced moderation switch.
*
* @returns {void}
*/
_onToggleModeration() {
this.setState(state => {
return {
audioModerationEnabled: !state.audioModerationEnabled,
content: this.props.t(state.audioModerationEnabled
? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
)
};
});
}
/**
* Implements {@code Component#render}.
*
@@ -39,8 +58,23 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
{ `${this.props.title} \n\n ${this.state.content}` }
</Text>
{this.props.exclude.length === 0 && <>
<Divider style = { styles.dividerWithSpacing } />
<View style = { styles.toggleContainer }>
<Text
style = {{
...this.props._dialogStyles.text,
...styles.toggleLabel
}}>
{this.props.t('dialog.moderationAudioLabel')}
</Text>
<Switch
onValueChange = { this._onToggleModeration }
value = { !this.state.audioModerationEnabled } />
</View>
</>}
</ConfirmDialog>
);
}

View File

@@ -1,7 +1,8 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { Switch, Text, View } from 'react-native';
import { Divider } from 'react-native-paper';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
@@ -12,6 +13,8 @@ import AbstractMuteEveryonesVideoDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog';
import styles from './styles';
type Props = AbstractProps & {
/**
@@ -24,10 +27,26 @@ type Props = AbstractProps & {
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteEveryoneDialog
* @extends AbstractMuteEveryonesVideoDialog
*/
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
/**
* Toggles advanced moderation switch.
*
* @returns {void}
*/
_onToggleModeration() {
this.setState(state => {
return {
moderationEnabled: !state.moderationEnabled,
content: this.props.t(state.moderationEnabled
? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
)
};
});
}
/**
* Implements {@code Component#render}.
*
@@ -39,8 +58,21 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
okKey = 'dialog.muteEveryonesVideoDialogOk'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
{ `${this.props.title} \n\n ${this.state.content}` }
</Text>
{this.props.exclude.length === 0 && <>
<Divider style = { styles.dividerWithSpacing } />
<View style = { styles.toggleContainer }>
<Text
style = {{ ...this.props._dialogStyles.text,
...styles.toggleLabel }}>
{this.props.t('dialog.moderationVideoLabel')}
</Text>
<Switch
onValueChange = { this._onToggleModeration }
value = { !this.state.moderationEnabled } />
</View>
</>}
</ConfirmDialog>
);
}

View File

@@ -18,6 +18,7 @@ import { PrivateMessageButton } from '../../../chat';
import { hideRemoteVideoMenu } from '../../actions.native';
import ConnectionStatusButton from '../native/ConnectionStatusButton';
import AskUnmuteButton from './AskUnmuteButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
@@ -126,6 +127,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
onCancel = { this._onCancel }
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
<AskUnmuteButton { ...buttonProps } />
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
<MuteEveryoneElseButton { ...buttonProps } />
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }

View File

@@ -67,5 +67,24 @@ export default createStyleSheet({
divider: {
backgroundColor: BaseTheme.palette.dividerColor
},
dividerWithSpacing: {
backgroundColor: BaseTheme.palette.dividerColor,
marginVertical: BaseTheme.spacing[3]
},
toggleContainer: {
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
width: '100%',
overflow: 'hidden'
},
toggleLabel: {
marginRight: BaseTheme.spacing[3],
maxWidth: '70%'
}
});