Compare commits

..

53 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
Дамян Минков
62e5d6c139 fix: Fixes undefined error causing to stop reloads.
Jibri was hitting a problem where it reloads and in certain cases (remote user is screensharing) we hit this participant undefined, which stops reload and stops recording.
It is still not obvious why we try to render this on leaving the conference and for a participant that is not in the conference ... this re-render should not happen as this component should be removed from its parent when the participant is not existing.
2021-09-17 15:04:32 -05:00
Saúl Ibarra Corretgé
06d8956bdb android: fix initializing soloader from Activity context
It has to be an application context.
2021-09-17 13:43:45 +02:00
Avram Tudor
adbb5f8ead fix(prejoin) implement ux improvements for mobile (#9939) 2021-09-17 13:12:34 +03:00
Hristo Terezov
32ed2bccec fix(Filmstrip) fix not being able to scroll 2021-09-17 11:22:20 +02:00
Jaya Allamsetty
58d98ad7d3 fix(device-selection) Do not create preview when mic selection is disabled.
This fixes an issue on mobile Safari when audio is lost after the user opens the device selection menu.
2021-09-16 15:41:44 -04:00
Vlad Piersec
e278703c58 fix(context-menus) Don't show volume slider on iOS web 2021-09-16 14:56:55 +03:00
Jaya Allamsetty
042a2cb447 fix(filmstrip) Fix filmstrip on RN when thumbnail reordering is disabled. 2021-09-15 11:29:46 -05:00
Saúl Ibarra Corretgé
a8f281a43e feat((rn,config) add a/b test flag to enable XMPP WebSockets on mobile 2021-09-15 15:26:25 +02:00
paweldomas
663752be2c feat(rn,connection) enable XMPP WebSocket on mobile
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-09-15 15:26:25 +02:00
Vlad Piersec
380ef3da0b fix(feedback): Scroll to the top when opening feedback dialog 2021-09-15 15:21:56 +03:00
Saúl Ibarra Corretgé
07da5940a5 fix(rn,conference) show underlay when tapping navbar buttons 2021-09-15 13:35:33 +02:00
robertpin
ab366b9d94 fix(av-moderation) Advanced moderation improvements (#9935)
* Update moderation in effect notifications

Only display one notification for each media type. Display notification for keyboard shortcuts as well

* Update muted remotely notification

Display name of moderator in the notification

* Fix indentation on moderation menu

* Update text for video moderation

* Added moderator label in participant pane

* Update microphone icon in participant list

For participants that speak, or are noisy, but aren't dominant speaker, the icon in the participant list will look the same as the dominant speaker icon but will not change their position in the list

* Added sound for asked to unmute notification

* Code review changes

* Code review changes

Use simple var instead of function for audio media state

* Move constants to constants file

* Moved constants from notifications to av-moderation
2021-09-15 11:28:44 +03:00
Pawel Domas
bba1917820 fix(conference.js): add tracks to the conference 2021-09-14 18:24:45 -04:00
Saúl Ibarra Corretgé
0833f8c867 feat(rn,conference) show invite button on the top navbar 2021-09-14 15:03:24 +02:00
Saúl Ibarra Corretgé
3bf9c41f08 fix(rn,conference) count fake participants when checking if lonely meeting
A shared video participant counts, so the lonely meeting invite options can
disappear.
2021-09-14 12:50:22 +02:00
Saúl Ibarra Corretgé
0b54e005d7 fix(rn,shared-video) validate URLs to avoid crashes on the native side 2021-09-14 12:50:22 +02:00
Saúl Ibarra Corretgé
b92c1f52d5 fix(shared-video) make placeholder translatable
Also fix capitalization.
2021-09-14 12:50:22 +02:00
135 changed files with 1726 additions and 950 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

@@ -174,7 +174,7 @@ class ReactInstanceManagerHolder {
return;
}
SoLoader.init(activity, /* native exopackage */ false);
SoLoader.init(activity.getApplication(), /* native exopackage */ false);
List<ReactPackage> packages
= new ArrayList<>(Arrays.asList(

View File

@@ -24,6 +24,8 @@ import {
redirectToStaticPage,
reloadWithStoredParams
} from './react/features/app/actions';
import { showModeratedNotification } from './react/features/av-moderation/actions';
import { shouldShowModeratedNotification } from './react/features/av-moderation/functions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -44,7 +46,8 @@ import {
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant
sendLocalParticipant,
_conferenceWillJoin
} from './react/features/base/conference';
import { getReplaceParticipant } from './react/features/base/config/functions';
import {
@@ -119,7 +122,7 @@ import {
maybeOpenFeedbackDialog,
submitFeedback
} from './react/features/feedback';
import { showNotification } from './react/features/notifications';
import { isModerationNotificationDisplayed, showNotification } from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import {
@@ -870,13 +873,24 @@ export default {
* dialogs in case of media permissions error.
*/
muteAudio(mute, showUI = true) {
const state = APP.store.getState();
if (!mute
&& isUserInteractionRequiredForUnmute(APP.store.getState())) {
&& isUserInteractionRequiredForUnmute(state)) {
logger.error('Unmuting audio requires user interaction');
return;
}
// check for A/V Moderation when trying to unmute
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, state)) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, state)) {
APP.store.dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
}
return;
}
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
// This will only modify base/media.audio.muted which is then synced
@@ -1315,14 +1329,14 @@ export default {
APP.conference.roomName,
this._getConferenceOptions());
APP.store.dispatch(conferenceWillJoin(room));
// Filter out the tracks that are muted (except on mobile Safari).
const tracks = isIosMobileBrowser() ? localTracks : localTracks.filter(track => !track.isMuted());
this._setLocalAudioVideoStreams(tracks);
this._room = room; // FIXME do not use this
APP.store.dispatch(_conferenceWillJoin(room));
sendLocalParticipant(APP.store, room);
this._setupListeners();

View File

@@ -45,9 +45,12 @@ var config = {
// disabled by the below option.
// enableThumbnailReordering: true,
// Enables XMPP WebSocket (as opposed to BOSH) for the given amount of users.
// mobileXmppWsThreshold: 10 // enable XMPP WebSockets on mobile for 10% of the users
// P2P test mode disables automatic switching to P2P when there are 2
// participants in the conference.
p2pTestMode: false
// p2pTestMode: false,
// Enables the test specific features consumed by jitsi-meet-torture
// testMode: false
@@ -71,8 +74,11 @@ var config = {
// callStatsThreshold: 5 // enable callstats for 5% of the users.
},
// Enables reactions feature.
// enableReactions: false,
// Disables moderator indicators.
// disableModeratorIndicator: false,
// Disables the reactions feature.
// disableReactions: true,
// Disables polls feature.
// disablePolls: false,
@@ -737,6 +743,7 @@ var config = {
// Array<string> of disabled sounds.
// Possible values:
// - 'ASKED_TO_UNMUTE_SOUND'
// - 'E2EE_OFF_SOUND'
// - 'E2EE_ON_SOUND'
// - 'INCOMING_MSG_SOUND'
@@ -900,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

@@ -106,9 +106,7 @@ export async function connect(id, password, roomName) {
serviceUrl += `?room=${roomName}`;
// FIXME Remove deprecated 'bosh' option assignment at some point(LJM will be accepting only 'serviceUrl' option
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
connectionConfig.serviceUrl = serviceUrl;
if (connectionConfig.websocketKeepAliveUrl) {
connectionConfig.websocketKeepAliveUrl += `?room=${roomName}`;

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

@@ -11,7 +11,7 @@
background-color: rgba(0, 0, 0, 0.7);
align-items: center;
display: flex;
padding: 8px 12px;
padding: 14px 16px;
}
&-circle {

View File

@@ -1,16 +1,25 @@
.device {
&-status {
align-items: center;
align-self: stretch;
color: #fff;
display: flex;
font-size: 14px;
font-weight: 400;
justify-content: center;
line-height: 20px;
margin-top: 8px;
padding: 6px;
text-align: center;
&-error {
align-items: flex-start;
background-color: #F8AE1A;
border-radius: 6px;
color: #040404;
padding: 12px 16px;
text-align: left;
}
span {
margin-left: 16px;
}
}
&-icon {
@@ -18,14 +27,8 @@
background-repeat: no-repeat;
display: inline-block;
height: 16px;
margin-right: 10px;
width: 16px;
&--warning {
svg path {
fill: rgba(241, 173, 51, 1);
}
}
&--ok {
svg path {
fill: #189b55;

View File

@@ -3,21 +3,16 @@
width: 100%;
}
&-checkbox-container {
margin-bottom: 16px;
width: 100%;
text-align: center;
}
&-error {
color: white;
background-color: #E04757;
border-radius: 6px;
padding: 4px;
box-sizing: border-box;
color: white;
font-size: 12px;
line-height: 16px;
margin-bottom: 16px;
margin-top: -8px;
font-size: 12px;
padding: 4px;
text-align: center;
width: 100%;
}

View File

@@ -16,6 +16,7 @@
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: 600;
line-height: 24px;
margin-bottom: 16px;
padding: 7px 16px;
@@ -128,10 +129,22 @@
#new-toolbox {
bottom: 0;
margin-bottom: 16px;
position: relative;
transition: none;
.toolbox-content {
margin-bottom: 4px;
}
.toolbox-content-items {
background: transparent;
border-radius: 0;
box-shadow: none;
display: flex;
justify-content: space-evenly;
padding: 8px 0;
}
.toolbox-content,
.toolbox-content-wrapper,
.toolbox-content-items {
@@ -163,17 +176,26 @@
padding: 16px;
width: 100%;
&-controls {
input.field {
font-size: 16px;
padding: 14px 16px;
}
}
.title {
font-size: 20px;
line-height: 28px;
letter-spacing: -0.012;
margin-bottom: 24px;
display: none;
}
}
.con-status {
margin: 16px;
width: calc(100% - 32px);
margin: 0;
width: 100%;
}
.device-status-error {
border-radius: 0;
margin: 0 -16px;
}
input.field {
@@ -183,15 +205,9 @@
.action-btn {
font-size: 16px;
margin-bottom: 8px;
padding: 11px 16px;
}
.toolbox-content-items {
border-radius: 0;
display: flex;
justify-content: space-evenly;
padding: 8px 0;
}
}
input::placeholder {
@@ -227,68 +243,3 @@
display: flex;
justify-content: center;
}
@mixin icon-container($bg, $fill) {
.toggle-button-icon-container {
background: $bg;
svg {
fill: $fill
}
}
}
.toggle-button {
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 13px;
height: 40px;
margin: 0 auto;
transition: background 0.16s ease-out;
@include flex-centered();
svg {
fill: transparent;
}
label {
cursor: pointer;
}
&:hover {
background: rgba(255, 255, 255, 0.1);
.toggle-button-icon-container {
display: none;
}
}
&-container {
position: relative;
@include flex-centered();
}
&-icon-container {
border-radius: 50%;
left: -22px;
padding: 2px;
position: absolute;
}
&--toggled {
@include icon-container(white, #1C2025);
&:hover {
.toggle-button-icon-container {
display: block;
}
}
.toggle-button-icon-container {
display: block;
}
}
}

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",
@@ -333,6 +335,7 @@
"shareScreenWarningH1": "If you want to share just your screen:",
"shareScreenWarningD1": "you need to stop audio sharing before sharing your screen.",
"shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.",
"sharedVideoLinkPlaceholder": "YouTube link or direct video link",
"stopLiveStreaming": "Stop live stream",
"stopRecording": "Stop recording",
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
@@ -565,9 +568,9 @@
"moderator": "You're now a moderator",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You've been muted by the moderator",
"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 the 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",
@@ -612,6 +615,7 @@
},
"actions": {
"allow": "Allow attendees to:",
"allowVideo": "Allow video",
"audioModeration": "Unmute themselves",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"invite": "Invite Someone",
@@ -622,7 +626,7 @@
"stopEveryonesVideo": "Stop everyone's video",
"stopVideo": "Stop video",
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
"videoModeration": "Start video"
"videoModeration": "Start their video"
}
},
"passwordSetRemotely": "Set by another participant",
@@ -695,6 +699,7 @@
"errorDialOutFailed": "Could not dial out. Call failed",
"errorDialOutStatus": "Error getting dial out status",
"errorMissingName": "Please enter your name to join the meeting",
"errorNoPermissions": "You need to enable microphone and camera access",
"errorStatusCode": "Error dialing out, status code: {{status}}",
"errorValidation": "Number validation failed",
"iWantToDialIn": "I want to dial in",

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

@@ -181,7 +181,7 @@ export function initAnalytics({ getState }: { getState: Function }, handlers: Ar
permanentProperties.appName = getAppName();
// Report if user is using websocket
permanentProperties.websocket = navigator.product !== 'ReactNative' && typeof config.websocket === 'string';
permanentProperties.websocket = typeof config.websocket === 'string';
// Report if user is using the external API
permanentProperties.externalApi = typeof API_ID === 'number';

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

@@ -17,3 +17,15 @@ export const MEDIA_TYPE_TO_PENDING_STORE_KEY: {[key: MediaType]: string} = {
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
};
export const ASKED_TO_UNMUTE_SOUND_ID = 'ASKED_TO_UNMUTE_SOUND';
export const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
export const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
export const MODERATION_NOTIFICATIONS = {
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.PRESENTER]: CS_MODERATION_NOTIFICATION_ID
};

View File

@@ -1,6 +1,7 @@
// @flow
import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { getConferenceState } from '../base/conference';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../base/media';
@@ -13,6 +14,7 @@ import {
raiseHand
} from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import {
hideNotification,
showNotification
@@ -35,21 +37,31 @@ import {
participantApproved,
participantPendingAudio
} from './actions';
import {
ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
CS_MODERATION_NOTIFICATION_ID,
VIDEO_MODERATION_NOTIFICATION_ID
} from './constants';
import {
isEnabledFromState,
isParticipantApproved,
isParticipantPending
} from './functions';
const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
import { ASKED_TO_UNMUTE_FILE } from './sounds';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { type } = action;
const { conference } = getConferenceState(getState());
switch (type) {
case APP_WILL_MOUNT: {
dispatch(registerSound(ASKED_TO_UNMUTE_SOUND_ID, ASKED_TO_UNMUTE_FILE));
break;
}
case APP_WILL_UNMOUNT: {
dispatch(unregisterSound(ASKED_TO_UNMUTE_SOUND_ID));
break;
}
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
let descriptionKey;
let titleKey;
@@ -160,6 +172,7 @@ StateListenerRegistry.register(
customActionNameKey: 'notify.unmute',
customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
}));
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
}
});

View File

@@ -0,0 +1,6 @@
/**
* The name of the bundled audio file which will be played for the raise hand sound.
*
* @type {string}
*/
export const ASKED_TO_UNMUTE_FILE = 'asked-unmute.mp3';

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));
}
}
@@ -357,7 +360,7 @@ export function conferenceUniqueIdSet(conference: Object) {
* the local participant will (try to) join.
* @returns {Function}
*/
function _conferenceWillJoin(conference: Object) {
export function _conferenceWillJoin(conference: Object) {
return (dispatch: Dispatch<any>, getState: Function) => {
const localTracks
= getLocalTracks(getState()['features/base/tracks'])

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

@@ -275,10 +275,17 @@ function _constructOptions(state) {
// redux store.
const options = _.cloneDeep(state['features/base/config']);
// Normalize the BOSH URL.
let { bosh } = options;
let { bosh, websocket } = options;
if (bosh) {
// TESTING: Only enable WebSocket for some percentage of users.
if (websocket) {
if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
websocket = undefined;
}
}
// Normalize the BOSH URL.
if (bosh && !websocket) {
const { locationURL } = state['features/base/connection'];
if (bosh.startsWith('//')) {
@@ -295,14 +302,24 @@ function _constructOptions(state) {
// eslint-disable-next-line max-len
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
}
// Append room to the URL's search.
const { room } = state['features/base/conference'];
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;
room && (bosh += `?room=${getBackendSafeRoomName(room)}`);
logger.log(`Using service URL ${serviceUrl}`);
// FIXME Remove deprecated 'bosh' option assignment at some point.
options.serviceUrl = options.bosh = bosh;
// Append room to the URL's search.
const { room } = state['features/base/conference'];
if (serviceUrl && room) {
const roomName = getBackendSafeRoomName(room);
options.serviceUrl = `${serviceUrl}?room=${roomName}`;
if (options.websocketKeepAliveUrl) {
options.websocketKeepAliveUrl += `?room=${roomName}`;
}
}
return options;

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

@@ -28,8 +28,7 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
*
* @static
*/
type Props = {
...DialogProps,
type Props = DialogProps & {
/**
* Custom dialog header that replaces the standard heading.
@@ -77,6 +76,11 @@ type Props = {
*/
onDecline?: Function,
/**
* Callback invoked when setting the ref of the Dialog.
*/
onDialogRef?: Function,
/**
* Disables rendering of the submit button.
*/
@@ -127,7 +131,7 @@ class StatelessDialog extends Component<Props> {
this._onKeyPress = this._onKeyPress.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._renderFooter = this._renderFooter.bind(this);
this._setDialogElement = this._setDialogElement.bind(this);
this._onDialogRef = this._onDialogRef.bind(this);
}
/**
@@ -166,7 +170,7 @@ class StatelessDialog extends Component<Props> {
width = { width || 'medium' }>
<div
onKeyPress = { this._onKeyPress }
ref = { this._setDialogElement }>
ref = { this._onDialogRef }>
<form
className = 'modal-dialog-form'
id = 'modal-dialog-form'
@@ -319,19 +323,18 @@ class StatelessDialog extends Component<Props> {
);
}
_setDialogElement: (?HTMLElement) => void;
_onDialogRef: (?Element) => void;
/**
* Sets the instance variable for the div containing the component's dialog
* element so it can be accessed directly.
* Callback invoked when setting the ref of the dialog's child passing the Modal ref.
* It is done this way because we cannot directly access the ref of the Modal component.
*
* @param {HTMLElement} element - The DOM element for the component's
* dialog.
* @param {HTMLElement} element - The DOM element for the dialog.
* @private
* @returns {void}
*/
_setDialogElement(element: ?HTMLElement) {
this._dialogElement = element;
_onDialogRef(element: ?Element) {
this.props.onDialogRef && this.props.onDialogRef(element && element.parentNode);
}
_onKeyPress: (Object) => void;

View File

@@ -0,0 +1,3 @@
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2332 14.3254L9.70838 0.910718C9.63451 0.77902 9.52397 0.670633 9.38965 0.598199C8.99846 0.387243 8.50693 0.52716 8.29177 0.910713L0.766813 14.3254C0.701168 14.4424 0.666748 14.5738 0.666748 14.7073C0.666748 15.1451 1.02867 15.4999 1.47512 15.4999H16.5249C16.6611 15.4999 16.7951 15.4662 16.9145 15.4018C17.3057 15.1909 17.4484 14.7089 17.2332 14.3254ZM2.84224 13.9147L9.00002 2.93733L15.1577 13.9147H2.84224ZM8.19177 11.5371C8.19177 11.0993 8.54663 10.7445 8.98437 10.7445H9.01591C9.45365 10.7445 9.80851 11.0993 9.80851 11.5371C9.80851 11.9748 9.45365 12.3297 9.01591 12.3297H8.98437C8.54663 12.3297 8.19177 11.9748 8.19177 11.5371ZM9.00014 6.7815C8.55369 6.7815 8.19177 7.14341 8.19177 7.58986V9.14351C8.19177 9.58996 8.55369 9.95188 9.00014 9.95188C9.44659 9.95188 9.80851 9.58996 9.80851 9.14351V7.58986C9.80851 7.14341 9.44659 6.7815 9.00014 6.7815Z" fill="#040404"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -51,6 +51,7 @@ export { default as IconEmail } from './envelope.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExclamation } from './exclamation.svg';
export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconFullScreen } from './full-screen.svg';

View File

@@ -4,6 +4,7 @@ import type { Dispatch } from 'redux';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { isModerationNotificationDisplayed } from '../../notifications';
import {
SET_AUDIO_MUTED,
@@ -113,7 +114,9 @@ export function setVideoMuted(
// check for A/V Moderation when trying to unmute
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
if (!isModerationNotificationDisplayed(MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
}
return;
}

View File

@@ -466,7 +466,7 @@ export function participantUpdated(participant = {}) {
* @returns {Promise}
*/
export function participantMutedUs(participant, track) {
return dispatch => {
return (dispatch, getState) => {
if (!participant) {
return;
}
@@ -474,7 +474,10 @@ export function participantMutedUs(participant, track) {
const isAudio = track.isAudioTrack();
dispatch(showNotification({
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle'
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
titleArguments: {
moderator: getParticipantDisplayName(getState, participant.getId())
}
}));
};
}

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

@@ -1,64 +0,0 @@
// @flow
import React, { useCallback } from 'react';
import { Icon, IconCheck } from '../../../icons';
const mainClass = 'toggle-button';
type Props = {
/**
* Text of the button.
*/
children: React$Node,
/**
* If the button is toggled or not.
*/
isToggled?: boolean,
/**
* OnClick button handler.
*/
onClick: Function
}
/**
* Button used as a toggle.
*
* @returns {ReactElement}
*/
function ToggleButton({ children, isToggled, onClick }: Props) {
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
const onKeyPressHandler = useCallback(e => {
if (onClick && (e.key === ' ')) {
e.preventDefault();
onClick();
}
}, [ onClick ]);
return (
<div
aria-checked = { isToggled }
className = { className }
id = 'toggle-button'
onClick = { onClick }
onKeyPress = { onKeyPressHandler }
role = 'switch'
tabIndex = { 0 }>
<div className = 'toggle-button-container'>
<div className = 'toggle-button-icon-container'>
<Icon
className = 'toggle-button-icon'
size = { 10 }
src = { IconCheck } />
</div>
<label htmlFor = 'toggle-button'>{children}</label>
</div>
</div>
);
}
export default ToggleButton;

View File

@@ -3,4 +3,3 @@
export { default as ActionButton } from './ActionButton';
export { default as InputField } from './InputField';
export { default as PreMeetingScreen } from './PreMeetingScreen';
export { default as ToggleButton } from './ToggleButton';

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

@@ -3,7 +3,7 @@
import UIEvents from '../../../../service/UI/UIEvents';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { hideNotification } from '../../notifications';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getAvailableDevices } from '../devices/actions';
import {
@@ -142,7 +142,9 @@ MiddlewareRegistry.register(store => next => action => {
// check for A/V Moderation when trying to start screen sharing
if ((action.enabled || action.enabled === undefined)
&& shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) {
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
}
return;
}

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

@@ -7,7 +7,7 @@ import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { getFeatureFlag, INVITE_ENABLED } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { Icon, IconAddPeople } from '../../../base/icons';
import { getParticipantCount } from '../../../base/participants';
import { getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import { doInvitePeople } from '../../../invite/actions.native';
@@ -131,7 +131,7 @@ function _mapStateToProps(state): $Shape<Props> {
return {
_isInviteFunctionsDiabled: !flag || disableInviteFunctions,
_isLonelyMeeting: conference && getParticipantCount(state) === 1,
_isLonelyMeeting: conference && getParticipantCountWithFake(state) === 1,
_styles: ColorSchemeRegistry.get(state, 'Conference')
};
}

View File

@@ -6,6 +6,7 @@ import { Text, View } from 'react-native';
import { getConferenceName } from '../../../base/conference';
import { getFeatureFlag, CONFERENCE_TIMER_ENABLED, MEETING_NAME_ENABLED } from '../../../base/flags';
import { connect } from '../../../base/redux';
import InviteButton from '../../../invite/components/add-people-dialog/native/InviteButton';
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import ConferenceTimer from '../ConferenceTimer';
@@ -54,8 +55,7 @@ const NavigationBar = (props: Props) => {
pointerEvents = 'box-none'
style = { styles.navBarWrapper }>
<View style = { styles.pipButtonContainer }>
<PictureInPictureButton
styles = { styles.pipButton } />
<PictureInPictureButton styles = { styles.pipButton } />
</View>
<View
pointerEvents = 'box-none'
@@ -78,6 +78,9 @@ const NavigationBar = (props: Props) => {
}
<Labels />
</View>
<View style = { styles.inviteButtonContainer }>
<InviteButton styles = { styles.inviteButton } />
</View>
</View>
);
};

View File

@@ -3,6 +3,8 @@ import { BoxModel, ColorPalette, fixAndroidViewClipping } from '../../../base/st
export const INSECURE_ROOM_NAME_LABEL_COLOR = ColorPalette.warning;
const NAVBAR_BUTTON_SIZE = 24;
/**
* The styles of the feature conference.
*/
@@ -29,6 +31,21 @@ export default {
flexDirection: 'row'
},
inviteButtonContainer: {
position: 'absolute',
top: 0,
right: 0,
zIndex: 1
},
inviteButton: {
iconStyle: {
padding: 10,
color: ColorPalette.white,
fontSize: NAVBAR_BUTTON_SIZE
},
underlayColor: ColorPalette.buttonUnderlay
},
lonelyButton: {
alignItems: 'center',
@@ -55,17 +72,18 @@ export default {
pipButtonContainer: {
position: 'absolute',
top: 10,
left: 5,
top: 0,
left: 0,
zIndex: 1
},
pipButton: {
iconStyle: {
padding: 10,
color: ColorPalette.white,
fontSize: 24
fontSize: NAVBAR_BUTTON_SIZE
},
underlayColor: 'transparent'
underlayColor: ColorPalette.buttonUnderlay
},
navBarSafeView: {
@@ -117,7 +135,8 @@ export default {
},
roomNameWrapper: {
flexDirection: 'row'
flexDirection: 'row',
marginHorizontal: 35
},
/**

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

@@ -258,6 +258,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
const { hideAudioInputPreview } = this.props;
if (hideAudioInputPreview) {
return;
}
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => {

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

@@ -36,6 +36,10 @@ const SCORES = [
'feedback.veryGood'
];
type Scrollable = {
scroll: Function
}
/**
* The type of the React {@code Component} props of {@link FeedbackDialog}.
*/
@@ -171,6 +175,12 @@ class FeedbackDialog extends Component<Props, State> {
this._onScoreContainerMouseLeave
= this._onScoreContainerMouseLeave.bind(this);
this._onSubmit = this._onSubmit.bind(this);
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
this._onScrollTop = (node: ?Scrollable) => {
node && node.scroll && node.scroll(0, 0);
};
}
/**
@@ -244,6 +254,7 @@ class FeedbackDialog extends Component<Props, State> {
<Dialog
okKey = 'dialog.Submit'
onCancel = { this._onCancel }
onDialogRef = { this._onScrollTop }
onSubmit = { this._onSubmit }
titleKey = 'feedback.rateExperience'>
<div className = 'feedback-dialog'>
@@ -364,6 +375,8 @@ class FeedbackDialog extends Component<Props, State> {
return true;
}
_onScrollTop: (node: ?Scrollable) => void;
}
/**

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

@@ -34,11 +34,6 @@ import ThumbnailWrapper from './ThumbnailWrapper';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* Fixes case in which context menu overflows and creates a scroll on the whole filmstrip videos pane.
*/
const TILEVIEW_VIDEO_PANES_STYLE = { overflow: 'visible' };
/**
* The type of the React {@code Component} props of {@link Filmstrip}.
*/
@@ -69,6 +64,11 @@ type Props = {
*/
_filmstripHeight: number,
/**
* Whether this is a recorder or not.
*/
_iAmRecorder: boolean,
/**
* Whether the filmstrip button is enabled.
*/
@@ -240,14 +240,14 @@ class Filmstrip extends PureComponent <Props> {
* @returns {Object}
*/
_calculateIndices(startIndex, stopIndex) {
const { _currentLayout, _thumbnailsReordered } = this.props;
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
let start = startIndex;
let stop = stopIndex;
if (_thumbnailsReordered) {
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
// endpoint. The remote participants start from index 1.
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
start = Math.max(startIndex - 1, 0);
stop = stopIndex - 1;
}
@@ -299,18 +299,24 @@ class Filmstrip extends PureComponent <Props> {
* @returns {string} - The key.
*/
_gridItemKey({ columnIndex, rowIndex }) {
const { _columns, _remoteParticipants, _remoteParticipantsLength, _thumbnailsReordered } = this.props;
const {
_columns,
_iAmRecorder,
_remoteParticipants,
_remoteParticipantsLength,
_thumbnailsReordered
} = this.props;
const index = (rowIndex * _columns) + columnIndex;
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
const remoteIndex = _thumbnailsReordered ? index - 1 : index;
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
if (index > _remoteParticipantsLength) {
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
return `empty-${index}`;
}
if (index === localIndex) {
if (!_iAmRecorder && index === localIndex) {
return 'local';
}
@@ -391,7 +397,6 @@ class Filmstrip extends PureComponent <Props> {
overscanRowCount = { 1 }
rowCount = { _rows }
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
style = { TILEVIEW_VIDEO_PANES_STYLE }
width = { _filmstripWidth }>
{
ThumbnailWrapper
@@ -534,7 +539,7 @@ class Filmstrip extends PureComponent <Props> {
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { testing = {} } = state['features/base/config'];
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
@@ -602,6 +607,7 @@ function _mapStateToProps(state) {
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,

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

@@ -29,7 +29,7 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(setTileViewDimensions());
break;
case PARTICIPANT_JOINED: {
updateRemoteParticipants(store);
updateRemoteParticipants(store, action.participant?.id);
break;
}
}

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

@@ -1,5 +1,7 @@
// @flow
import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants';
import { MEDIA_TYPE } from '../base/media';
import { toState } from '../base/redux';
declare var interfaceConfig: Object;
@@ -26,3 +28,18 @@ export function areThereNotifications(stateful: Object | Function) {
export function joinLeaveNotificationsDisabled() {
return Boolean(typeof interfaceConfig !== 'undefined' && interfaceConfig?.DISABLE_JOIN_LEAVE_NOTIFICATIONS);
}
/**
* Returns whether or not the moderation notification for the given type is displayed.
*
* @param {MEDIA_TYPE} mediaType - The media type to check.
* @param {Object | Function} stateful - The redux store state.
* @returns {boolean}
*/
export function isModerationNotificationDisplayed(mediaType: MEDIA_TYPE, stateful: Object | Function) {
const state = toState(stateful);
const { notifications } = state['features/notifications'];
return Boolean(notifications.find(n => n.uid === MODERATION_NOTIFICATIONS[mediaType]));
}

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

@@ -55,10 +55,10 @@ const useStyles = makeStyles(() => {
},
text: {
color: '#C2C2C2',
padding: '10px 16px 10px 52px'
padding: '10px 16px'
},
paddedAction: {
marginLeft: '36px;'
marginLeft: '36px'
}
};
});

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>

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