Compare commits
32 Commits
update-gap
...
jitihouse/
Author | SHA1 | Date |
---|---|---|
xenia | 4bd73cc368 | |
Boris Grozev | e12999d44f | |
Robert Pintilii | 8982f17ce1 | |
Robert Pintilii | c8f1690057 | |
Robert Pintilii | aa57309057 | |
damencho | fb81619fc5 | |
Hristo Terezov | 5a5656020b | |
Hristo Terezov | 0ff44a2f22 | |
Hristo Terezov | 4d04ea325e | |
Hristo Terezov | 42ce6dcc58 | |
Hristo Terezov | b033d0268a | |
Hristo Terezov | 4aea40d34f | |
Hristo Terezov | e5a170fb28 | |
Hristo Terezov | d1cf5578fc | |
Hristo Terezov | 4b29af6b5f | |
bgrozev | f3481576ff | |
bgrozev | 455a91a5c6 | |
Gabriel Borlea | 297ab194a8 | |
Christoph Settgast | 077a88a803 | |
Gabriel Borlea | 02c232440e | |
Emmanuel Pelletier | f727b9295f | |
Robert Pintilii | 0d0bec3aad | |
Emmanuel Pelletier | cfb8589bef | |
japm48 | 65730e256e | |
Robert Pintilii | 7b8b911fee | |
Robert Pintilii | 036286a1d6 | |
Robert Pintilii | d550254f31 | |
Robert Pintilii | b1a71d55d7 | |
George Politis | 17ed45799c | |
Jaya Allamsetty | e5681382b0 | |
Robert Pintilii | c27cb25afe | |
Avram Tudor | baf5aa14e8 |
15
config.js
15
config.js
|
@ -48,7 +48,7 @@ var config = {
|
|||
// BOSH URL. FIXME: use XEP-0156 to discover it.
|
||||
bosh: 'https://jitsi-meet.example.com/' + subdir + 'http-bind',
|
||||
|
||||
// Websocket URL
|
||||
// Websocket URL (XMPP)
|
||||
// websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
|
||||
|
||||
// The real JID of focus participant - can be overridden here
|
||||
|
@ -56,6 +56,19 @@ var config = {
|
|||
// https://github.com/jitsi/jitsi-meet/issues/7376
|
||||
// focusUserJid: 'focus@auth.jitsi-meet.example.com',
|
||||
|
||||
// Options related to the bridge (colibri) data channel
|
||||
bridgeChannel: {
|
||||
// If the backend advertises multiple colibri websockets, this options allows
|
||||
// to filter some of them out based on the domain name. We use the first URL
|
||||
// which does not match ignoreDomain, falling back to the first one that matches
|
||||
// ignoreDomain. Has no effect if undefined.
|
||||
// ignoreDomain: 'example.com',
|
||||
|
||||
// Prefer SCTP (WebRTC data channels over the media path) over a colibri websocket.
|
||||
// If SCTP is available in the backend it will be used instead of a WS. Defaults to
|
||||
// false (SCTP is used only if available and no WS are available).
|
||||
// preferSctp: false
|
||||
},
|
||||
|
||||
// Testing / experimental features.
|
||||
//
|
||||
|
|
|
@ -22,11 +22,7 @@
|
|||
}
|
||||
|
||||
&-entry-text {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 213px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
&.left-margin {
|
||||
margin-left: 36px;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
@keyframes rotateAroundY {
|
||||
from { transform: rotateY(0deg); }
|
||||
to { transform: rotateY(360deg); }
|
||||
}
|
||||
|
||||
@keyframes rainbowRoad {
|
||||
to {
|
||||
background-position: 400% 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Comic Sans MS", "Comic Sans", sans-serif !important;
|
||||
}
|
||||
|
||||
a {
|
||||
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
-moz-background-clip: text !important;
|
||||
-moz-text-fill-color: transparent !important;
|
||||
animation: rainbowRoad 8s linear infinite !important;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
|
||||
}
|
||||
|
||||
.dominant-speaker {
|
||||
box-shadow: inset 0px 0px 0px 4px rgba(255,0,255,0.33) !important;
|
||||
}
|
||||
|
||||
.display-avatar-only {
|
||||
background-image: url("");
|
||||
}
|
||||
|
||||
.videocontainer:nth-child(odd) {
|
||||
transform: rotate(1.5deg);
|
||||
}
|
||||
|
||||
.videocontainer:nth-child(even) {
|
||||
transform: rotate(-1.3deg);
|
||||
}
|
||||
|
||||
#largeVideoContainer.videocontainer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sideToolbarContainer {
|
||||
transform: rotate(-1.1deg);
|
||||
}
|
||||
|
||||
.displayname:before {
|
||||
content: "♡︎ ";
|
||||
}
|
||||
|
||||
.displayname:after {
|
||||
content: " ♡︎";
|
||||
}
|
||||
|
||||
.avatar, .userAvatar {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.avatar:hover, .userAvatar:hover {
|
||||
animation: rotateAroundY 3.6s linear infinite;
|
||||
}
|
|
@ -82,6 +82,7 @@
|
|||
}
|
||||
|
||||
.left-column {
|
||||
order: -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 0;
|
||||
|
@ -92,6 +93,7 @@
|
|||
.right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
padding-left: 16px;
|
||||
padding-top: 13px;
|
||||
|
@ -99,11 +101,11 @@
|
|||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #5E6D7A;
|
||||
|
@ -125,8 +127,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.with-click-handler:hover,
|
||||
&.with-click-handler:focus {
|
||||
&.with-click-handler:hover {
|
||||
background-color: #c7ddff;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#polls-panel {
|
||||
.polls-panel {
|
||||
height: calc(100% - 119px);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ body.welcome-page {
|
|||
font-size: 14px;
|
||||
padding-left: 10px;
|
||||
|
||||
&:focus {
|
||||
&.focus-visible {
|
||||
outline: auto 2px #005fcc;
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ body.welcome-page {
|
|||
margin: 4px;
|
||||
display: $welcomePageTabButtonsDisplay;
|
||||
|
||||
.tab {
|
||||
[role="tab"] {
|
||||
background-color: #c7ddff;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
|
@ -176,8 +176,10 @@ body.welcome-page {
|
|||
margin: 2px;
|
||||
padding: 7px 0;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
|
||||
&.selected {
|
||||
&[aria-selected="true"] {
|
||||
background-color: #FFF;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'reload_overlay/reload_overlay';
|
||||
@import 'mini_toolbox';
|
||||
@import 'modals/desktop-picker/desktop-picker';
|
||||
@import 'modals/device-selection/device-selection';
|
||||
@import 'modals/dialog';
|
||||
@import 'modals/embed-meeting/embed-meeting';
|
||||
@import 'modals/feedback/feedback';
|
||||
|
@ -95,3 +94,9 @@ $flagsImagePath: "../images/";
|
|||
@import 'notifications';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
/* Jeet crew BEGIN */
|
||||
|
||||
@import 'jiti';
|
||||
|
||||
/* Jeet crew END */
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
.device-selection {
|
||||
.device-selectors {
|
||||
font-size: 14px;
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.device-selector-icon {
|
||||
align-self: center;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.device-selector-label {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
|
||||
.device-selector-trigger {
|
||||
background-color: #0E1624;
|
||||
border: 1px solid #455166;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
height: 2.3em;
|
||||
justify-content: space-between;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.device-selector-trigger-disabled {
|
||||
.device-selector-trigger {
|
||||
color: #a5adba;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selector-trigger-text {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-column {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&.column-selectors {
|
||||
margin-left: 15px;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.column-video {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-video-container {
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.video-input-preview {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
|
||||
> video {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.video-input-preview-error {
|
||||
color: $participantNameColor;
|
||||
display: none;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&.video-preview-has-error {
|
||||
background: black;
|
||||
|
||||
.video-input-preview-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.video-input-preview-display {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio-output-preview {
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
color: #6FB1EA;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #B3D4FF;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-input-preview {
|
||||
background: #1B2638;
|
||||
border-radius: 5px;
|
||||
height: 8px;
|
||||
|
||||
.audio-input-preview-level {
|
||||
background: #75B1FF;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
-webkit-transition: width .1s ease-in-out;
|
||||
-moz-transition: width .1s ease-in-out;
|
||||
-o-transition: width .1s ease-in-out;
|
||||
transition: width .1s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection.video-hidden {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.column-selectors {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.column-video {
|
||||
order: 1;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
|
@ -44,61 +44,3 @@
|
|||
-webkit-animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-out
|
||||
}
|
||||
|
||||
.feedback-dialog {
|
||||
margin-bottom: 5px;
|
||||
|
||||
.details {
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
background-color: $feedbackInputBg;
|
||||
color: $feedbackInputTextColor;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.rating {
|
||||
line-height: 1.2;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
.star-label {
|
||||
font-size: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.star-btn {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 34px;
|
||||
outline: none;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@include transition(all .2s ease);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&.starHover {
|
||||
color: #36B37E;
|
||||
};
|
||||
|
||||
}
|
||||
.star-btn:focus,
|
||||
.star-btn:active {
|
||||
outline: 1px solid #B8C7E0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
*/
|
||||
|
||||
var interfaceConfig = {
|
||||
APP_NAME: 'Jitsi Meet',
|
||||
APP_NAME: 'JitSea 🏴☠️',
|
||||
AUDIO_LEVEL_PRIMARY_COLOR: 'rgba(255,255,255,0.4)',
|
||||
AUDIO_LEVEL_SECONDARY_COLOR: 'rgba(255,255,255,0.2)',
|
||||
|
||||
|
|
|
@ -184,13 +184,21 @@
|
|||
"deepLinking": {
|
||||
"appNotInstalled": "Sie benötigen die „{{app}}“-App, um der Konferenz auf dem Smartphone beizutreten.",
|
||||
"description": "Nichts passiert? Wir haben versucht, die Konferenz in {{app}} zu öffnen. Versuchen Sie es erneut oder treten Sie der Konferenz in {{app}} im Web bei.",
|
||||
"descriptionNew": "Nichts passiert? Wir haben versucht, die Konferenz in {{app}} zu öffnen. <br /><br /> Versuchen Sie es erneut oder treten Sie der Konferenz im Web bei.",
|
||||
"descriptionWithoutWeb": "Ist nichts passiert? Wir haben versucht, Ihre Besprechung in der „{{app}}“-Desktop-App zu starten.",
|
||||
"downloadApp": "App herunterladen",
|
||||
"downloadMobileApp": "Aus dem App Store herunterladen",
|
||||
"ifDoNotHaveApp": "Wenn Sie die App noch nicht haben:",
|
||||
"ifHaveApp": "Wenn Sie die App bereits haben:",
|
||||
"joinInApp": "Mit der App am Meeting teilnehmen",
|
||||
"joinInAppNew": "Mit der App",
|
||||
"joinInBrowser": "Im Browser",
|
||||
"launchMeetingLabel": "Wie möchten Sie an der Konferenz teilnehmen?",
|
||||
"launchWebButton": "Im Web öffnen",
|
||||
"noMobileApp": "Sie haben die App noch nicht installiert?",
|
||||
"termsAndConditions": "Indem Sie fortfahren, stimmen Sie underen<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>Nutzungsbedingungen</a> zu.",
|
||||
"title": "Die Konferenz wird in {{app}} geöffnet …",
|
||||
"titleNew": "Konferenz starten ...",
|
||||
"tryAgainButton": "Erneut mit der nativen Applikation versuchen",
|
||||
"unsupportedBrowser": "Sie verwenden einen Browser, der noch nicht unterstützt wird."
|
||||
},
|
||||
|
@ -203,6 +211,12 @@
|
|||
"microphonePermission": "Fehler beim Bezug der Mikrofon-Zugriffsberechtigungen"
|
||||
},
|
||||
"deviceSelection": {
|
||||
"hid": {
|
||||
"callControl": "Anrufsteuerung",
|
||||
"connectedDevices": "Verbundene Geräte:",
|
||||
"deleteDevice": "Gerät löschen",
|
||||
"pairDevice": "Gerät verbinden"
|
||||
},
|
||||
"noPermission": "Berechtigungen nicht erteilt",
|
||||
"previewUnavailable": "Keine Vorschau verfügbar",
|
||||
"selectADevice": "Ein Gerät wählen",
|
||||
|
@ -226,7 +240,9 @@
|
|||
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
|
||||
"Yes": "Ja",
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Livestream"
|
||||
"close": "Popup schließen",
|
||||
"liveStreaming": "Livestream",
|
||||
"sharingTabs": "Optionen zum Teilen"
|
||||
},
|
||||
"add": "Hinzufügen",
|
||||
"addMeetingNote": "Notiz zu dieser Konferenz hinzufügen",
|
||||
|
@ -438,6 +454,11 @@
|
|||
"veryBad": "Sehr schlecht",
|
||||
"veryGood": "Sehr gut"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Videominiaturen"
|
||||
}
|
||||
},
|
||||
"giphy": {
|
||||
"noResults": "Keine Ergebnisse :(",
|
||||
"search": "GIPHY durchsuchen"
|
||||
|
@ -751,6 +772,7 @@
|
|||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Anwesende ({{count}})",
|
||||
"visitors": "Gäste ({{count}})",
|
||||
"waitingLobby": "In der Lobby ({{count}})"
|
||||
},
|
||||
"search": "Suche Anwesende",
|
||||
|
@ -758,6 +780,7 @@
|
|||
},
|
||||
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
|
||||
"passwordSetRemotely": "von einer anderen Person gesetzt",
|
||||
"pinParticipant": "{{participantName}} - anheften",
|
||||
"pinnedParticipant": "Die Person ist angeheftet",
|
||||
"polls": {
|
||||
"answer": {
|
||||
|
@ -950,6 +973,7 @@
|
|||
"title": "Sicherheitsoptionen"
|
||||
},
|
||||
"settings": {
|
||||
"audio": "Audio",
|
||||
"buttonLabel": "Einstellungen",
|
||||
"calendar": {
|
||||
"about": "Die Kalenderintegration von {{appName}} wird verwendet, um ein sicheres Zugreifen auf Ihren Kalender und Auslesen der bevorstehenden Termine zu ermöglichen.",
|
||||
|
@ -970,9 +994,11 @@
|
|||
"maxStageParticipants": "Maximale Anzahl an Personen, die zur Hauptansicht angeheftet werden können",
|
||||
"microphones": "Mikrofon",
|
||||
"moderator": "Moderation",
|
||||
"moderatorOptions": "Moderationseinstellungen",
|
||||
"more": "Mehr",
|
||||
"name": "Name",
|
||||
"noDevice": "Kein",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"participantJoined": "Neue Person nimmt teil",
|
||||
"participantKnocking": "Person hat Lobby betreten",
|
||||
"participantLeft": "Person verlässt die Konferenz",
|
||||
|
@ -983,13 +1009,14 @@
|
|||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"selfView": "Eigene Ansicht",
|
||||
"sounds": "Hinweistöne",
|
||||
"shortcuts": "Tastaturkürzel",
|
||||
"speakers": "Lautsprecher",
|
||||
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
|
||||
"startReactionsMuted": "Interaktionstöne für alle deaktivieren",
|
||||
"startVideoMuted": "Alle Personen treten ohne Video bei",
|
||||
"talkWhileMuted": "Wenn bei Stummschaltung gesprochen wird",
|
||||
"title": "Einstellungen"
|
||||
"title": "Einstellungen",
|
||||
"video": "Kamera"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Erweitert",
|
||||
|
@ -1081,6 +1108,7 @@
|
|||
"giphy": "GIPHY ein-/ausschalten",
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"hangup": "Konferenz verlassen",
|
||||
"heading": "Toolbar",
|
||||
"help": "Hilfe",
|
||||
"invite": "Person einladen",
|
||||
"kick": "Person entfernen",
|
||||
|
@ -1147,6 +1175,7 @@
|
|||
"download": "Unsere Apps herunterladen",
|
||||
"e2ee": "Ende-zu-Ende-Verschlüsselung",
|
||||
"embedMeeting": "Konferenz einbetten",
|
||||
"enableNoiseSuppression": "Rauschunterdrückung einschalten",
|
||||
"endConference": "Konferenz für alle beenden",
|
||||
"enterFullScreen": "Vollbildmodus",
|
||||
"enterTileView": "Kachelansicht einschalten",
|
||||
|
@ -1234,6 +1263,7 @@
|
|||
"subtitlesOff": "Ausschalten",
|
||||
"tr": "TR"
|
||||
},
|
||||
"unpinParticipant": "{{participantName}} - Nicht mehr anheften",
|
||||
"userMedia": {
|
||||
"androidGrantPermissions": "Wählen Sie <b><i>Zulassen</i></b>, wenn der Browser um Berechtigungen bittet.",
|
||||
"chromeGrantPermissions": "Wählen Sie <b><i>Zulassen</i></b>, wenn der Browser um Berechtigungen bittet.",
|
||||
|
@ -1272,9 +1302,11 @@
|
|||
"ldTooltip": "Video wird in niedriger Auflösung angezeigt",
|
||||
"lowDefinition": "Niedrige Auflösung",
|
||||
"performanceSettings": "Qualitätseinstellungen",
|
||||
"recording": "Aufnahme läuft",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Video wird in Standardauflösung angezeigt",
|
||||
"standardDefinition": "Standardauflösung"
|
||||
"standardDefinition": "Standardauflösung",
|
||||
"streaming": "Streaming läuft"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Verbindungsinformationen",
|
||||
|
@ -1324,6 +1356,7 @@
|
|||
"webAssemblyWarning": "WebAssembly wird nicht unterstützt",
|
||||
"webAssemblyWarningDescription": "WebAssembly ist deaktiviert oder wird in diesem Browser nicht unterstützt"
|
||||
},
|
||||
"visitorsLabel": "Anzahl Gäste: {{count}}",
|
||||
"volumeSlider": "Lautstärkeregler",
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
|
@ -1356,6 +1389,7 @@
|
|||
"microsoftLogo": "Microsoft Logo",
|
||||
"policyLogo": "Richtlinienlogo"
|
||||
},
|
||||
"meetingsAccessibilityLabel": "Konferenzen",
|
||||
"mobileDownLoadLinkAndroid": "Android App Download",
|
||||
"mobileDownLoadLinkFDroid": "F-Droid App Download",
|
||||
"mobileDownLoadLinkIos": "iOS App Download",
|
||||
|
@ -1375,5 +1409,10 @@
|
|||
"terms": "AGB",
|
||||
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
|
||||
"upcomingMeetings": "Ihre zukünftigen Konferenzen"
|
||||
},
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Whiteboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -838,7 +838,7 @@
|
|||
"selectCamera": "Cámara",
|
||||
"selectMic": "Micrófono",
|
||||
"sounds": "Sonidos",
|
||||
"speakers": "Parlantes",
|
||||
"speakers": "Altavoces",
|
||||
"startAudioMuted": "Todos inician silenciados",
|
||||
"startVideoMuted": "Todos inician con cámara desactivada",
|
||||
"talkWhileMuted": "Hablar en silencio",
|
||||
|
|
|
@ -220,7 +220,7 @@
|
|||
"noPermission": "Permission not granted",
|
||||
"previewUnavailable": "Preview unavailable",
|
||||
"selectADevice": "Select a device",
|
||||
"testAudio": "Play a test sound"
|
||||
"testAudio": "Test"
|
||||
},
|
||||
"dialIn": {
|
||||
"screenTitle": "Dial-in summary"
|
||||
|
@ -240,7 +240,9 @@
|
|||
"WaitingForHostTitle": "Waiting for the host ...",
|
||||
"Yes": "Yes",
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Live Stream"
|
||||
"close": "Close dialog",
|
||||
"liveStreaming": "Live Stream",
|
||||
"sharingTabs": "Sharing options"
|
||||
},
|
||||
"add": "Add",
|
||||
"addMeetingNote": "Add a note about this meeting",
|
||||
|
@ -886,9 +888,9 @@
|
|||
},
|
||||
"profile": {
|
||||
"avatar": "avatar",
|
||||
"setDisplayNameLabel": "Set your display name",
|
||||
"setDisplayNameLabel": "Name",
|
||||
"setEmailInput": "Enter email",
|
||||
"setEmailLabel": "Set your gravatar email",
|
||||
"setEmailLabel": "Gravatar email",
|
||||
"title": "Profile"
|
||||
},
|
||||
"raisedHand": "Would like to speak",
|
||||
|
@ -971,6 +973,7 @@
|
|||
"title": "Security Options"
|
||||
},
|
||||
"settings": {
|
||||
"audio": "Audio",
|
||||
"buttonLabel": "Settings",
|
||||
"calendar": {
|
||||
"about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
|
||||
|
@ -991,9 +994,11 @@
|
|||
"maxStageParticipants": "Maximum number of participants who can be pinned to the main stage (EXPERIMENTAL)",
|
||||
"microphones": "Microphones",
|
||||
"moderator": "Moderator",
|
||||
"more": "More",
|
||||
"moderatorOptions": "Moderator options",
|
||||
"more": "General",
|
||||
"name": "Name",
|
||||
"noDevice": "None",
|
||||
"notifications": "Notifications",
|
||||
"participantJoined": "Participant Joined",
|
||||
"participantKnocking": "Participant entered lobby",
|
||||
"participantLeft": "Participant Left",
|
||||
|
@ -1004,13 +1009,14 @@
|
|||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
"selfView": "Self view",
|
||||
"sounds": "Sounds",
|
||||
"shortcuts": "Shortcuts",
|
||||
"speakers": "Speakers",
|
||||
"startAudioMuted": "Everyone starts muted",
|
||||
"startReactionsMuted": "Mute reaction sounds for everyone",
|
||||
"startVideoMuted": "Everyone starts hidden",
|
||||
"talkWhileMuted": "Talk while muted",
|
||||
"title": "Settings"
|
||||
"title": "Settings",
|
||||
"video": "Video"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Advanced",
|
||||
|
@ -1169,6 +1175,7 @@
|
|||
"download": "Download our apps",
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"embedMeeting": "Embed meeting",
|
||||
"enableNoiseSuppression": "Enable noise suppression",
|
||||
"endConference": "End meeting for all",
|
||||
"enterFullScreen": "View full screen",
|
||||
"enterTileView": "Enter tile view",
|
||||
|
@ -1343,7 +1350,7 @@
|
|||
"none": "None",
|
||||
"pleaseWait": "Please wait...",
|
||||
"removeBackground": "Remove background",
|
||||
"slightBlur": "Slight Blur",
|
||||
"slightBlur": "Half Blur",
|
||||
"title": "Virtual backgrounds",
|
||||
"uploadedImage": "Uploaded image {{index}}",
|
||||
"webAssemblyWarning": "WebAssembly not supported",
|
||||
|
@ -1382,6 +1389,7 @@
|
|||
"microsoftLogo": "Microsoft logo",
|
||||
"policyLogo": "Policy logo"
|
||||
},
|
||||
"meetingsAccessibilityLabel": "Meetings",
|
||||
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
|
||||
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
|
||||
"mobileDownLoadLinkIos": "Download mobile app for iOS",
|
||||
|
|
|
@ -106,6 +106,8 @@ import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/fea
|
|||
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture';
|
||||
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
|
||||
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
|
||||
import { SETTINGS_TABS } from '../../react/features/settings/constants';
|
||||
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
|
||||
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
|
||||
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
|
||||
|
@ -113,7 +115,6 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
|
|||
import { setTileView, toggleTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality';
|
||||
import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
|
||||
|
@ -798,7 +799,8 @@ function initCommands() {
|
|||
APP.store.dispatch(overwriteConfig(whitelistedConfig));
|
||||
},
|
||||
'toggle-virtual-background': () => {
|
||||
APP.store.dispatch(toggleDialog(VirtualBackgroundDialog));
|
||||
APP.store.dispatch(toggleDialog(SettingsDialog, {
|
||||
defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND }));
|
||||
},
|
||||
'end-conference': () => {
|
||||
APP.store.dispatch(endConference());
|
||||
|
|
|
@ -292,17 +292,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
|||
// Used by torture.
|
||||
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
|
||||
|
||||
/**
|
||||
* Updates the displayed avatar for participant.
|
||||
*
|
||||
* @param {string} id - User id whose avatar should be updated.
|
||||
* @param {string} avatarURL - The URL to avatar image to display.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.refreshAvatarDisplay = function(id) {
|
||||
VideoLayout.changeUserAvatar(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify user that connection failed.
|
||||
* @param {string} stropheErrorMsg raw Strophe error message
|
||||
|
|
|
@ -139,12 +139,6 @@ const VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
}
|
||||
},
|
||||
|
||||
isLargeVideoVisible() {
|
||||
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
|
||||
},
|
||||
|
|
|
@ -8,10 +8,9 @@ import {
|
|||
createShortcutEvent,
|
||||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { toggleDialog } from '../../react/features/base/dialog';
|
||||
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||
import { KeyboardShortcutsDialog }
|
||||
from '../../react/features/keyboard-shortcuts';
|
||||
import { openSettingsDialog } from '../../react/features/settings/actions';
|
||||
import { SETTINGS_TABS } from '../../react/features/settings/constants';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
|
@ -120,15 +119,17 @@ const KeyboardShortcut = {
|
|||
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
|
||||
},
|
||||
|
||||
getShortcutsDescriptions() {
|
||||
return _shortcutsHelp;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the {@KeyboardShortcutsDialog} dialog.
|
||||
* Opens the {@SettingsDialog} dialog on the Shortcuts page.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
openDialog() {
|
||||
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
|
||||
shortcutDescriptions: _shortcutsHelp
|
||||
}));
|
||||
APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
@ -158,7 +158,7 @@
|
|||
"circular-dependency-plugin": "5.2.0",
|
||||
"clean-css-cli": "4.3.0",
|
||||
"css-loader": "3.6.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"eslint-plugin-jsdoc": "37.0.3",
|
||||
|
@ -3250,15 +3250,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
|
||||
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz",
|
||||
"integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.4.0",
|
||||
"globals": "^13.15.0",
|
||||
"globals": "^13.19.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -3279,9 +3279,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "13.17.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
|
||||
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
|
||||
"version": "13.20.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
|
||||
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
|
@ -3333,6 +3333,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz",
|
||||
"integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/js-analytics": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz",
|
||||
|
@ -3470,14 +3479,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
|
||||
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.4"
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
|
@ -9885,14 +9894,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
|
||||
"integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz",
|
||||
"integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": "^1.3.3",
|
||||
"@humanwhocodes/config-array": "^0.10.5",
|
||||
"@eslint/eslintrc": "^2.0.0",
|
||||
"@eslint/js": "8.35.0",
|
||||
"@humanwhocodes/config-array": "^0.11.8",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
|
@ -9903,19 +9914,19 @@
|
|||
"eslint-utils": "^3.0.0",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.4.0",
|
||||
"esquery": "^1.4.0",
|
||||
"esquery": "^1.4.2",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globals": "^13.15.0",
|
||||
"globby": "^11.1.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"globals": "^13.19.0",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.0.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"js-sdsl": "^4.1.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
|
@ -10324,9 +10335,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/globals": {
|
||||
"version": "13.17.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
|
||||
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
|
||||
"version": "13.20.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
|
||||
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
|
@ -10454,9 +10465,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
|
||||
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
|
||||
"version": "9.4.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
|
||||
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.0",
|
||||
|
@ -10492,9 +10503,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
|
||||
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
|
||||
"integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"estraverse": "^5.1.0"
|
||||
|
@ -13405,8 +13416,8 @@
|
|||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
|
@ -22631,15 +22642,15 @@
|
|||
}
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
|
||||
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz",
|
||||
"integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.4.0",
|
||||
"globals": "^13.15.0",
|
||||
"globals": "^13.19.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -22654,9 +22665,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.17.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
|
||||
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
|
||||
"version": "13.20.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
|
||||
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-fest": "^0.20.2"
|
||||
|
@ -22689,6 +22700,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz",
|
||||
"integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==",
|
||||
"dev": true
|
||||
},
|
||||
"@giphy/js-analytics": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz",
|
||||
|
@ -22810,14 +22827,14 @@
|
|||
}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
|
||||
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.4"
|
||||
"minimatch": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/module-importer": {
|
||||
|
@ -27604,14 +27621,16 @@
|
|||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
|
||||
"integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz",
|
||||
"integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/eslintrc": "^1.3.3",
|
||||
"@humanwhocodes/config-array": "^0.10.5",
|
||||
"@eslint/eslintrc": "^2.0.0",
|
||||
"@eslint/js": "8.35.0",
|
||||
"@humanwhocodes/config-array": "^0.11.8",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
|
@ -27622,19 +27641,19 @@
|
|||
"eslint-utils": "^3.0.0",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.4.0",
|
||||
"esquery": "^1.4.0",
|
||||
"esquery": "^1.4.2",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globals": "^13.15.0",
|
||||
"globby": "^11.1.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"globals": "^13.19.0",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.0.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"js-sdsl": "^4.1.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
|
@ -27716,9 +27735,9 @@
|
|||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.17.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
|
||||
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
|
||||
"version": "13.20.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
|
||||
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-fest": "^0.20.2"
|
||||
|
@ -28030,9 +28049,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"espree": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
|
||||
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
|
||||
"version": "9.4.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
|
||||
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"acorn": "^8.8.0",
|
||||
|
@ -28054,9 +28073,9 @@
|
|||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"esquery": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
|
||||
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
|
||||
"integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"estraverse": "^5.1.0"
|
||||
|
@ -30289,8 +30308,8 @@
|
|||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
@ -163,7 +163,7 @@
|
|||
"circular-dependency-plugin": "5.2.0",
|
||||
"clean-css-cli": "4.3.0",
|
||||
"css-loader": "3.6.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"eslint-plugin-jsdoc": "37.0.3",
|
||||
|
|
|
@ -5,7 +5,6 @@ import '../base/media/middleware';
|
|||
import '../dynamic-branding/middleware';
|
||||
import '../e2ee/middleware';
|
||||
import '../external-api/middleware';
|
||||
import '../keyboard-shortcuts/middleware';
|
||||
import '../no-audio-signal/middleware';
|
||||
import '../notifications/middleware';
|
||||
import '../noise-detection/middleware';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 814 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5.77465L10.9742 9.05416C10.6778 10.0019 9.79505 10.6433 8.80206 10.6323L5.36607 10.5945L8.16808 12.5835C8.97785 13.1583 9.31502 14.1961 8.99778 15.1371L7.90002 18.3932L10.6576 16.343C11.4545 15.7505 12.5456 15.7505 13.3425 16.343L16.1001 18.3932L15.0023 15.1371C14.6851 14.1961 15.0222 13.1583 15.832 12.5835L18.634 10.5945L15.198 10.6323C14.205 10.6433 13.3223 10.0019 13.0258 9.05416L12 5.77465ZM12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1602 8.24439C13.3281 8.16225 14.25 7.18879 14.25 6C14.25 4.75736 13.2426 3.75 12 3.75C10.7574 3.75 9.75 4.75736 9.75 6C9.75 7.18151 10.6607 8.15032 11.8184 8.24278C11.5197 8.21896 11.2375 8.13687 10.9831 8.00775C9.85816 9.83693 8.19346 10.3318 6.74461 10.3424C6.66364 9.17333 5.68961 8.25 4.5 8.25C3.25736 8.25 2.25 9.25736 2.25 10.5C2.25 11.7426 3.25736 12.75 4.5 12.75C5.32135 12.75 6.03995 12.31 6.43279 11.6528C6.39557 11.715 6.35542 11.7754 6.31253 11.8336C6.67901 11.8506 7.06503 11.8447 7.4618 11.805C8.64456 11.6868 9.95784 11.2623 11.0918 10.2228C11.4736 9.87283 11.8205 9.46641 12.1289 9.0006C12.4727 9.47803 12.8468 9.89069 13.2474 10.2432C14.3929 11.2513 15.6592 11.6829 16.8118 11.8042C17.1152 11.8362 17.4101 11.8467 17.6932 11.8412C17.6739 11.8152 17.6551 11.7887 17.6369 11.7619C18.0415 12.3582 18.725 12.75 19.5 12.75C20.7426 12.75 21.75 11.7426 21.75 10.5C21.75 9.25736 20.7426 8.25 19.5 8.25C18.3129 8.25 17.3405 9.16938 17.256 10.335C15.9245 10.2658 14.3912 9.7053 13.1957 7.90649C12.8918 8.09752 12.5389 8.21778 12.1602 8.24439ZM12 6.75C12.4142 6.75 12.75 6.41421 12.75 6C12.75 5.58579 12.4142 5.25 12 5.25C11.5858 5.25 11.25 5.58579 11.25 6C11.25 6.41421 11.5858 6.75 12 6.75ZM20.25 10.5C20.25 10.9142 19.9142 11.25 19.5 11.25C19.0858 11.25 18.75 10.9142 18.75 10.5C18.75 10.0858 19.0858 9.75 19.5 9.75C19.9142 9.75 20.25 10.0858 20.25 10.5ZM4.5 11.25C4.91421 11.25 5.25 10.9142 5.25 10.5C5.25 10.0858 4.91421 9.75 4.5 9.75C4.08579 9.75 3.75 10.0858 3.75 10.5C3.75 10.9142 4.08579 11.25 4.5 11.25Z" fill="white"/>
|
||||
<path d="M17.9485 12.1296C18.3135 12.4773 18.7952 12.7036 19.3289 12.7437L19.2591 12.9706C19.1623 13.2853 18.8715 13.5 18.5423 13.5C18.0377 13.5 17.677 13.0117 17.8254 12.5294L17.9485 12.1296Z" fill="white"/>
|
||||
<path d="M12.75 18.0001C13.1642 18.0001 13.5 18.3359 13.5 18.7501C13.5 19.1643 13.1642 19.5001 12.75 19.5001H7.85792C7.19941 19.5001 6.61791 19.0706 6.42425 18.4412L4.6712 12.7437C5.20487 12.7036 5.6866 12.4773 6.05164 12.1296L7.85792 18.0001H12.75Z" fill="white"/>
|
||||
<path d="M11.25 18.0002C10.8358 18.0002 10.5 18.336 10.5 18.7502C10.5 19.1644 10.8358 19.5002 11.25 19.5002H16.1421C16.8006 19.5002 17.3821 19.0707 17.5757 18.4413L19.3289 12.7437C18.7952 12.7036 18.3135 12.4773 17.9485 12.1296L16.1421 18.0002H11.25Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -39,10 +39,13 @@ export { default as IconExclamationSolid } from './exclamation-solid.svg';
|
|||
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
|
||||
export { default as IconExitFullscreen } from './exit-fullscreen.svg';
|
||||
export { default as IconFaceSmile } from './face-smile.svg';
|
||||
export { default as IconFavorite } from './favorite.svg';
|
||||
export { default as IconFavoriteSolid } from './favorite-solid.svg';
|
||||
export { default as IconFeedback } from './feedback.svg';
|
||||
export { default as IconGear } from './gear.svg';
|
||||
export { default as IconGoogle } from './google.svg';
|
||||
export { default as IconHangup } from './hangup.svg';
|
||||
export { default as IconHost } from './host.svg';
|
||||
export { default as IconHelp } from './help.svg';
|
||||
export { default as IconHighlight } from './highlight.svg';
|
||||
export { default as IconImage } from './image.svg';
|
||||
|
|
|
@ -88,7 +88,11 @@ const _updateLastN = debounce(({ dispatch, getState }: IStore) => {
|
|||
lastNSelected = 1;
|
||||
}
|
||||
|
||||
dispatch(setLastN(lastNSelected));
|
||||
const { lastN } = state['features/base/lastn'];
|
||||
|
||||
if (lastN !== lastNSelected) {
|
||||
dispatch(setLastN(lastNSelected));
|
||||
}
|
||||
}, 1000); /* Don't send this more often than once a second. */
|
||||
|
||||
|
||||
|
|
|
@ -15,11 +15,15 @@ export type MediaType = 'audio' | 'video' | 'screenshare';
|
|||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const MEDIA_TYPE: { [key: string]: MediaType; } = {
|
||||
AUDIO: 'audio',
|
||||
SCREENSHARE: 'screenshare',
|
||||
VIDEO: 'video'
|
||||
};
|
||||
export const MEDIA_TYPE: {
|
||||
AUDIO: MediaType;
|
||||
SCREENSHARE: MediaType;
|
||||
VIDEO: MediaType;
|
||||
} = {
|
||||
AUDIO: 'audio',
|
||||
SCREENSHARE: 'screenshare',
|
||||
VIDEO: 'video'
|
||||
};
|
||||
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
|
|
@ -601,7 +601,7 @@ export function getDominantSpeakerParticipant(stateful: IStateful) {
|
|||
export function isEveryoneModerator(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
return state.everyoneIsModerator === true;
|
||||
return state.numberOfNonModeratorParticipants === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -429,11 +429,12 @@ StateListenerRegistry.register(
|
|||
'e2ee.enabled': (participant: IJitsiParticipant, value: string) =>
|
||||
_e2eeUpdated(store, conference, participant.getId(), value),
|
||||
'features_e2ee': (participant: IJitsiParticipant, value: boolean) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
e2eeSupported: value
|
||||
})),
|
||||
getParticipantById(store.getState(), participant.getId())?.e2eeSupported !== value
|
||||
&& store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
e2eeSupported: value
|
||||
})),
|
||||
'features_jigasi': (participant: IJitsiParticipant, value: boolean) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
|
@ -506,7 +507,12 @@ StateListenerRegistry.register(
|
|||
function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConference,
|
||||
participantId: string, newValue: string | boolean) {
|
||||
const e2eeEnabled = newValue === 'true';
|
||||
const { e2ee = {} } = getState()['features/base/config'];
|
||||
const state = getState();
|
||||
const { e2ee = {} } = state['features/base/config'];
|
||||
|
||||
if (e2eeEnabled === getParticipantById(state, participantId)?.e2eeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(participantUpdated({
|
||||
conference,
|
||||
|
@ -641,7 +647,6 @@ function _participantJoinedOrUpdated(store: IStore, next: Function, action: any)
|
|||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
if (typeof raisedHandTimestamp !== 'undefined') {
|
||||
|
||||
if (local) {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
const rHand = parseInt(raisedHandTimestamp, 10);
|
||||
|
@ -691,14 +696,6 @@ function _participantJoinedOrUpdated(store: IStore, next: Function, action: any)
|
|||
}
|
||||
}
|
||||
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
||||
|
||||
// Force update of local video getting a new id.
|
||||
APP.UI.refreshAvatarDisplay(currentKnownId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,10 +63,12 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
|||
|
||||
const DEFAULT_STATE = {
|
||||
dominantSpeaker: undefined,
|
||||
everyoneIsModerator: false,
|
||||
fakeParticipants: new Map(),
|
||||
local: undefined,
|
||||
localScreenShare: undefined,
|
||||
numberOfNonModeratorParticipants: 0,
|
||||
numberOfParticipantsDisabledE2EE: 0,
|
||||
numberOfParticipantsNotSupportingE2EE: 0,
|
||||
overwrittenNameList: {},
|
||||
pinnedParticipant: undefined,
|
||||
raisedHandsQueue: [],
|
||||
|
@ -79,10 +81,12 @@ const DEFAULT_STATE = {
|
|||
|
||||
export interface IParticipantsState {
|
||||
dominantSpeaker?: string;
|
||||
everyoneIsModerator: boolean;
|
||||
fakeParticipants: Map<string, IParticipant>;
|
||||
local?: ILocalParticipant;
|
||||
localScreenShare?: IParticipant;
|
||||
numberOfNonModeratorParticipants: number;
|
||||
numberOfParticipantsDisabledE2EE: number;
|
||||
numberOfParticipantsNotSupportingE2EE: number;
|
||||
overwrittenNameList: { [id: string]: string; };
|
||||
pinnedParticipant?: string;
|
||||
raisedHandsQueue: Array<{ id: string; raisedHandTimestamp: number; }>;
|
||||
|
@ -200,23 +204,30 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
}
|
||||
|
||||
let newParticipant: IParticipant | null = null;
|
||||
const oldParticipant = local || state.local?.id === id ? state.local : state.remote.get(id);
|
||||
|
||||
if (state.remote.has(id)) {
|
||||
newParticipant = _participant(state.remote.get(id), action);
|
||||
newParticipant = _participant(oldParticipant, action);
|
||||
state.remote.set(id, newParticipant);
|
||||
} else if (id === state.local?.id) {
|
||||
newParticipant = state.local = _participant(state.local, action);
|
||||
}
|
||||
|
||||
if (newParticipant) {
|
||||
|
||||
// everyoneIsModerator calculation:
|
||||
if (oldParticipant && newParticipant && !newParticipant.fakeParticipant) {
|
||||
const isModerator = isParticipantModerator(newParticipant);
|
||||
|
||||
if (state.everyoneIsModerator && !isModerator) {
|
||||
state.everyoneIsModerator = false;
|
||||
} else if (!state.everyoneIsModerator && isModerator) {
|
||||
state.everyoneIsModerator = _isEveryoneModerator(state);
|
||||
if (isParticipantModerator(oldParticipant) !== isModerator) {
|
||||
state.numberOfNonModeratorParticipants += isModerator ? -1 : 1;
|
||||
}
|
||||
|
||||
const e2eeEnabled = Boolean(newParticipant.e2eeEnabled);
|
||||
const e2eeSupported = Boolean(newParticipant.e2eeSupported);
|
||||
|
||||
if (Boolean(oldParticipant.e2eeEnabled) !== e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE += e2eeEnabled ? -1 : 1;
|
||||
}
|
||||
if (!local && Boolean(oldParticipant.e2eeSupported) !== e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE += e2eeSupported ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,13 +278,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
state.dominantSpeaker = id;
|
||||
}
|
||||
|
||||
const isModerator = isParticipantModerator(participant);
|
||||
const { local, remote } = state;
|
||||
if (!fakeParticipant) {
|
||||
const isModerator = isParticipantModerator(participant);
|
||||
|
||||
if (state.everyoneIsModerator && !isModerator) {
|
||||
state.everyoneIsModerator = false;
|
||||
} else if (!local && remote.size === 0 && isModerator) {
|
||||
state.everyoneIsModerator = true;
|
||||
if (!isModerator) {
|
||||
state.numberOfNonModeratorParticipants += 1;
|
||||
}
|
||||
|
||||
const { e2eeEnabled, e2eeSupported } = participant as IParticipant;
|
||||
|
||||
if (!e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE += 1;
|
||||
}
|
||||
|
||||
if (!participant.local && !e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (participant.local) {
|
||||
|
@ -349,6 +369,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
pinnedParticipant
|
||||
} = state;
|
||||
let oldParticipant = remote.get(id);
|
||||
let isLocalScreenShare = false;
|
||||
|
||||
if (oldParticipant?.sources?.size) {
|
||||
const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
|
||||
|
@ -373,6 +394,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
oldParticipant = state.local;
|
||||
delete state.local;
|
||||
} else if (localScreenShare?.id === id) {
|
||||
isLocalScreenShare = true;
|
||||
oldParticipant = state.local;
|
||||
delete state.localScreenShare;
|
||||
} else {
|
||||
|
@ -383,10 +405,6 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
state.sortedRemoteParticipants.delete(id);
|
||||
state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
|
||||
|
||||
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
|
||||
state.everyoneIsModerator = _isEveryoneModerator(state);
|
||||
}
|
||||
|
||||
if (dominantSpeaker === id) {
|
||||
state.dominantSpeaker = undefined;
|
||||
}
|
||||
|
@ -407,6 +425,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
|||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
}
|
||||
|
||||
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
|
||||
const { e2eeEnabled, e2eeSupported } = oldParticipant;
|
||||
|
||||
if (!isParticipantModerator(oldParticipant)) {
|
||||
state.numberOfNonModeratorParticipants -= 1;
|
||||
}
|
||||
|
||||
if (!e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE -= 1;
|
||||
}
|
||||
|
||||
if (!oldParticipant.local && !e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
case PARTICIPANT_SOURCES_UPDATED: {
|
||||
|
@ -465,27 +499,6 @@ function _getDisplayName(state: Object, name?: string): string {
|
|||
return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through the participants in the state in order to check if all participants are moderators.
|
||||
*
|
||||
* @param {Object} state - The local participant redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isEveryoneModerator(state: IParticipantsState) {
|
||||
if (isParticipantModerator(state.local)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ k, p ] of state.remote) {
|
||||
if (!isParticipantModerator(p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer function for a single participant.
|
||||
*
|
||||
|
|
|
@ -105,17 +105,14 @@ class MeetingsList extends Component<Props> {
|
|||
* @returns {React.ReactNode}
|
||||
*/
|
||||
render() {
|
||||
const { listEmptyComponent, meetings, t } = this.props;
|
||||
const { listEmptyComponent, meetings } = this.props;
|
||||
|
||||
/**
|
||||
* If there are no recent meetings we don't want to display anything.
|
||||
*/
|
||||
if (meetings) {
|
||||
return (
|
||||
<Container
|
||||
aria-label = { t('welcomepage.recentList') }
|
||||
className = 'meetings-list'
|
||||
tabIndex = '-1'>
|
||||
<Container className = 'meetings-list'>
|
||||
{
|
||||
meetings.length === 0
|
||||
? listEmptyComponent
|
||||
|
@ -237,23 +234,16 @@ class MeetingsList extends Component<Props> {
|
|||
|
||||
return (
|
||||
<Container
|
||||
aria-label = { title }
|
||||
className = { rootClassName }
|
||||
key = { index }
|
||||
onClick = { onPress }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Container className = 'left-column'>
|
||||
<Text className = 'title'>
|
||||
{ _toDateString(date) }
|
||||
</Text>
|
||||
<Text className = 'subtitle'>
|
||||
{ _toTimeString(time) }
|
||||
</Text>
|
||||
</Container>
|
||||
onClick = { onPress }>
|
||||
<Container className = 'right-column'>
|
||||
<Text className = 'title'>
|
||||
<Text
|
||||
className = 'title'
|
||||
onClick = { onPress }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ title }
|
||||
</Text>
|
||||
{
|
||||
|
@ -269,6 +259,14 @@ class MeetingsList extends Component<Props> {
|
|||
</Text>) : null
|
||||
}
|
||||
</Container>
|
||||
<Container className = 'left-column'>
|
||||
<Text className = 'title'>
|
||||
{ _toDateString(date) }
|
||||
</Text>
|
||||
<Text className = 'subtitle'>
|
||||
{ _toTimeString(time) }
|
||||
</Text>
|
||||
</Container>
|
||||
<Container className = 'actions'>
|
||||
{ elementAfter || null }
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ const useStyles = makeStyles()(theme => {
|
|||
backgroundColor: theme.palette.action01Active
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
'&.focus-visible': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@ const useStyles = makeStyles()(theme => {
|
|||
backgroundColor: theme.palette.ui02
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
'&.focus-visible': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
|
||||
},
|
||||
|
|
|
@ -5,6 +5,9 @@ import { makeStyles } from 'tss-react/mui';
|
|||
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { TEXT_OVERFLOW_TYPES } from '../../constants.any';
|
||||
|
||||
import TextWithOverflow from './TextWithOverflow';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
|
@ -23,6 +26,13 @@ export interface IProps {
|
|||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Id of dom element controlled by this item. Matches aria-controls.
|
||||
* Useful if you need this item as a tab element.
|
||||
*
|
||||
*/
|
||||
controls?: string;
|
||||
|
||||
/**
|
||||
* Custom icon. If used, the icon prop is ignored.
|
||||
* Used to allow custom children instead of just the default icons.
|
||||
|
@ -52,13 +62,23 @@ export interface IProps {
|
|||
/**
|
||||
* Keydown handler.
|
||||
*/
|
||||
onKeyDown?: (e?: React.KeyboardEvent) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
|
||||
/**
|
||||
* Keypress handler.
|
||||
*/
|
||||
onKeyPress?: (e?: React.KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Overflow type for item text.
|
||||
*/
|
||||
overflowType?: TEXT_OVERFLOW_TYPES;
|
||||
|
||||
/**
|
||||
* You can use this item as a tab. Defaults to button if not set.
|
||||
*/
|
||||
role?: 'tab' | 'button';
|
||||
|
||||
/**
|
||||
* Whether the item is marked as selected.
|
||||
*/
|
||||
|
@ -102,7 +122,7 @@ const useStyles = makeStyles()(theme => {
|
|||
backgroundColor: theme.palette.ui03
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
'&.focus-visible': {
|
||||
boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}`
|
||||
}
|
||||
},
|
||||
|
@ -142,6 +162,7 @@ const ContextMenuItem = ({
|
|||
accessibilityLabel,
|
||||
children,
|
||||
className,
|
||||
controls,
|
||||
customIcon,
|
||||
disabled,
|
||||
id,
|
||||
|
@ -149,6 +170,8 @@ const ContextMenuItem = ({
|
|||
onClick,
|
||||
onKeyDown,
|
||||
onKeyPress,
|
||||
overflowType,
|
||||
role = 'button',
|
||||
selected,
|
||||
testId,
|
||||
text,
|
||||
|
@ -158,8 +181,10 @@ const ContextMenuItem = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
aria-controls = { controls }
|
||||
aria-disabled = { disabled }
|
||||
aria-label = { accessibilityLabel }
|
||||
aria-selected = { role === 'tab' ? selected : undefined }
|
||||
className = { cx(styles.contextMenuItem,
|
||||
_overflowDrawer && styles.contextMenuItemDrawer,
|
||||
disabled && styles.contextMenuItemDisabled,
|
||||
|
@ -172,20 +197,24 @@ const ContextMenuItem = ({
|
|||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { disabled ? undefined : 0 }>
|
||||
role = { role }
|
||||
tabIndex = { role === 'tab'
|
||||
? selected ? 0 : -1
|
||||
: disabled ? undefined : 0
|
||||
}>
|
||||
{customIcon ? customIcon
|
||||
: icon && <Icon
|
||||
className = { styles.contextMenuItemIcon }
|
||||
size = { 20 }
|
||||
src = { icon } />}
|
||||
{text && (
|
||||
<span
|
||||
<TextWithOverflow
|
||||
className = { cx(styles.text,
|
||||
_overflowDrawer && styles.drawerText,
|
||||
textClassName) }>
|
||||
_overflowDrawer && styles.drawerText,
|
||||
textClassName) }
|
||||
overflowType = { overflowType } >
|
||||
{text}
|
||||
</span>
|
||||
</TextWithOverflow>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -23,12 +23,6 @@ const useStyles = makeStyles()(theme => {
|
|||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
closeIcon: {
|
||||
'&:focus': {
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
|
||||
title: {
|
||||
color: theme.palette.text01,
|
||||
...withPixelLineHeight(theme.typography.heading5),
|
||||
|
@ -137,8 +131,7 @@ const Dialog = ({
|
|||
</p>
|
||||
{!hideCloseButton && (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
className = { classes.closeIcon }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose } />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { MoveFocusInside } from 'react-focus-lock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
@ -105,6 +106,7 @@ const useStyles = makeStyles()(theme => {
|
|||
|
||||
backContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
alignItems: 'center',
|
||||
|
||||
'& > button': {
|
||||
|
@ -112,12 +114,6 @@ const useStyles = makeStyles()(theme => {
|
|||
}
|
||||
},
|
||||
|
||||
closeIcon: {
|
||||
'&:focus': {
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
|
@ -129,8 +125,14 @@ const useStyles = makeStyles()(theme => {
|
|||
}
|
||||
},
|
||||
|
||||
header: {
|
||||
order: -1,
|
||||
paddingBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
footer: {
|
||||
justifyContent: 'flex-end',
|
||||
paddingTop: theme.spacing(4),
|
||||
|
||||
'& button:last-child': {
|
||||
marginLeft: '16px'
|
||||
|
@ -143,20 +145,21 @@ interface IObject {
|
|||
[key: string]: string | string[] | boolean | number | number[] | {} | undefined;
|
||||
}
|
||||
|
||||
export interface IDialogTab {
|
||||
export interface IDialogTab<P> {
|
||||
cancel?: Function;
|
||||
className?: string;
|
||||
component: ComponentType<any>;
|
||||
icon: Function;
|
||||
labelKey: string;
|
||||
name: string;
|
||||
props?: IObject;
|
||||
propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
|
||||
propsUpdateFunction?: (tabState: IObject, newProps: P) => P;
|
||||
submit?: Function;
|
||||
}
|
||||
|
||||
interface IProps extends IBaseProps {
|
||||
defaultTab?: string;
|
||||
tabs: IDialogTab[];
|
||||
tabs: IDialogTab<any>[];
|
||||
}
|
||||
|
||||
const DialogWithTabs = ({
|
||||
|
@ -169,6 +172,7 @@ const DialogWithTabs = ({
|
|||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
|
||||
const [ userSelected, setUserSelected ] = useState(false);
|
||||
const [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
|
||||
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
||||
const [ isMobile, setIsMobile ] = useState(false);
|
||||
|
@ -189,18 +193,63 @@ const DialogWithTabs = ({
|
|||
}
|
||||
}, [ isMobile ]);
|
||||
|
||||
const back = useCallback(() => {
|
||||
setSelectedTab(undefined);
|
||||
const onUserSelection = useCallback((tabName?: string) => {
|
||||
setUserSelected(true);
|
||||
setSelectedTab(tabName);
|
||||
}, []);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
const back = useCallback(() => {
|
||||
onUserSelection(undefined);
|
||||
}, []);
|
||||
|
||||
|
||||
// the userSelected state is used to prevent setting focus when the user
|
||||
// didn't actually interact (for the first rendering for example)
|
||||
useEffect(() => {
|
||||
if (userSelected) {
|
||||
document.querySelector<HTMLElement>(isMobile
|
||||
? `.${classes.title}`
|
||||
: `#${`dialogtab-button-${selectedTab}`}`
|
||||
)?.focus();
|
||||
setUserSelected(false);
|
||||
}
|
||||
}, [ isMobile, userSelected, selectedTab ]);
|
||||
|
||||
const onClose = useCallback((isCancel = true) => {
|
||||
if (isCancel) {
|
||||
tabs.forEach(({ cancel }) => {
|
||||
cancel && dispatch(cancel());
|
||||
});
|
||||
}
|
||||
dispatch(hideDialog());
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((tabName: string) => () => {
|
||||
setSelectedTab(tabName);
|
||||
onUserSelection(tabName);
|
||||
}, []);
|
||||
|
||||
const onTabKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
let newTab: IDialogTab<any> | null = null;
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
newTab = index === 0 ? tabs[tabs.length - 1] : tabs[index - 1];
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
newTab = index === tabs.length - 1 ? tabs[0] : tabs[index + 1];
|
||||
}
|
||||
|
||||
if (newTab !== null) {
|
||||
onUserSelection(newTab.name);
|
||||
}
|
||||
}, [ tabs.length ]);
|
||||
|
||||
const onMobileKeyDown = useCallback((tabName: string) => (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
onUserSelection(tabName);
|
||||
}
|
||||
}, [ classes.contentContainer ]);
|
||||
|
||||
const getTabProps = (tabId: number) => {
|
||||
const tabConfiguration = tabs[tabId];
|
||||
const currentTabState = tabStates[tabId];
|
||||
|
@ -225,7 +274,7 @@ const DialogWithTabs = ({
|
|||
tabs.forEach(({ submit }, idx) => {
|
||||
submit?.(tabStates[idx]);
|
||||
});
|
||||
onClose();
|
||||
onClose(false);
|
||||
}, [ tabs, tabStates ]);
|
||||
|
||||
const selectedTabIndex = useMemo(() => {
|
||||
|
@ -257,8 +306,7 @@ const DialogWithTabs = ({
|
|||
|
||||
const closeIcon = useMemo(() => (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
className = { classes.closeIcon }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose } />
|
||||
|
@ -270,21 +318,39 @@ const DialogWithTabs = ({
|
|||
onClose = { onClose }
|
||||
size = 'large'>
|
||||
{(!isMobile || !selectedTab) && (
|
||||
<div className = { classes.sidebar }>
|
||||
<div
|
||||
aria-orientation = 'vertical'
|
||||
className = { classes.sidebar }
|
||||
role = { isMobile ? undefined : 'tablist' }>
|
||||
<div className = { classes.titleContainer }>
|
||||
<h2 className = { classes.title }>{t(titleKey ?? '')}</h2>
|
||||
<MoveFocusInside>
|
||||
<h2
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{t(titleKey ?? '')}
|
||||
</h2>
|
||||
</MoveFocusInside>
|
||||
{isMobile && closeIcon}
|
||||
</div>
|
||||
{tabs.map(tab => {
|
||||
{tabs.map((tab, index) => {
|
||||
const label = t(tab.labelKey);
|
||||
|
||||
/**
|
||||
* When not on mobile, the items behave as tabs,
|
||||
* that's why we set `controls`, `role` and `selected` attributes
|
||||
* only when not on mobile, they are useful only for the tab behavior.
|
||||
*/
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { label }
|
||||
className = { cx(isMobile && classes.menuItemMobile) }
|
||||
controls = { isMobile ? undefined : `dialogtab-content-${tab.name}` }
|
||||
icon = { tab.icon }
|
||||
id = { `dialogtab-button-${tab.name}` }
|
||||
key = { tab.name }
|
||||
onClick = { onClick(tab.name) }
|
||||
onKeyDown = { isMobile ? onMobileKeyDown(tab.name) : onTabKeyDown(index) }
|
||||
role = { isMobile ? undefined : 'tab' }
|
||||
selected = { tab.name === selectedTab }
|
||||
text = { label } />
|
||||
);
|
||||
|
@ -292,26 +358,45 @@ const DialogWithTabs = ({
|
|||
</div>
|
||||
)}
|
||||
{(!isMobile || selectedTab) && (
|
||||
<div className = { classes.contentContainer }>
|
||||
<div className = { classes.buttonContainer }>
|
||||
{isMobile && (
|
||||
<div
|
||||
className = { classes.contentContainer }
|
||||
tabIndex = { isMobile ? -1 : undefined }>
|
||||
{/* DOM order is important for keyboard users: show whole heading first when on mobile… */}
|
||||
{isMobile && (
|
||||
<div className = { cx(classes.buttonContainer, classes.header) }>
|
||||
<span className = { classes.backContainer }>
|
||||
<h2
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||
</h2>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.Back') }
|
||||
className = { classes.closeIcon }
|
||||
icon = { IconArrowBack }
|
||||
id = 'modal-header-back-button'
|
||||
onClick = { back } />
|
||||
<h2 className = { classes.title }>
|
||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||
</h2>
|
||||
</span>
|
||||
)}
|
||||
{closeIcon}
|
||||
</div>
|
||||
<div className = { classes.content }>
|
||||
{selectedTabComponent}
|
||||
</div>
|
||||
{closeIcon}
|
||||
</div>
|
||||
)}
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
aria-labelledby = { isMobile ? undefined : `${tab.name}-button` }
|
||||
className = { cx(classes.content, tab.name !== selectedTab && 'hide') }
|
||||
id = { `dialogtab-content-${tab.name}` }
|
||||
key = { tab.name }
|
||||
role = { isMobile ? undefined : 'tabpanel' }
|
||||
tabIndex = { isMobile ? -1 : 0 }>
|
||||
{ tab.name === selectedTab && selectedTabComponent }
|
||||
</div>
|
||||
))}
|
||||
{/* But show the close button *after* tab panels when not on mobile (using tabs).
|
||||
This is so that we can tab back and forth tab buttons and tab panels easily. */}
|
||||
{!isMobile && (
|
||||
<div className = { cx(classes.buttonContainer, classes.header) }>
|
||||
{closeIcon}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className = { cx(classes.buttonContainer, classes.footer) }>
|
||||
<Button
|
||||
|
|
|
@ -79,7 +79,7 @@ const useStyles = makeStyles()(theme => {
|
|||
width: '100%',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
padding: '8px 16px',
|
||||
padding: '10px 16px',
|
||||
paddingRight: '42px',
|
||||
border: 0,
|
||||
appearance: 'none',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { isMobileBrowser } from '../../../environment/utils';
|
||||
|
@ -11,6 +11,7 @@ interface ITabProps {
|
|||
selected: string;
|
||||
tabs: Array<{
|
||||
accessibilityLabel: string;
|
||||
controlsId: string;
|
||||
countBadge?: number;
|
||||
disabled?: boolean;
|
||||
id: string;
|
||||
|
@ -44,7 +45,7 @@ const useStyles = makeStyles()(theme => {
|
|||
borderColor: theme.palette.ui10
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
'&.focus-visible': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`,
|
||||
border: 0,
|
||||
|
@ -87,26 +88,52 @@ const Tabs = ({
|
|||
}: ITabProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const isMobile = isMobileBrowser();
|
||||
|
||||
const handleChange = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onChange(e.currentTarget.id);
|
||||
const onClick = useCallback(id => () => {
|
||||
onChange(id);
|
||||
}, []);
|
||||
const onKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
let newIndex: number | null = null;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
newIndex = index === 0 ? tabs.length - 1 : index - 1;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
newIndex = index === tabs.length - 1 ? 0 : index + 1;
|
||||
}
|
||||
|
||||
if (newIndex !== null) {
|
||||
onChange(tabs[newIndex].id);
|
||||
}
|
||||
}, [ tabs ]);
|
||||
|
||||
useEffect(() => {
|
||||
// this test is needed to make sure the effect is triggered because of user actually changing tab
|
||||
if (document.activeElement?.getAttribute('role') === 'tab') {
|
||||
document.querySelector<HTMLButtonElement>(`#${selected}`)?.focus();
|
||||
}
|
||||
}, [ selected ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label = { accessibilityLabel }
|
||||
className = { cx(classes.container, className) }
|
||||
role = 'tablist'>
|
||||
{tabs.map(tab => (
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
aria-controls = { tab.controlsId }
|
||||
aria-label = { tab.accessibilityLabel }
|
||||
aria-selected = { selected === tab.id }
|
||||
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
|
||||
disabled = { tab.disabled }
|
||||
id = { tab.id }
|
||||
key = { tab.id }
|
||||
onClick = { handleChange }
|
||||
role = 'tab'>
|
||||
onClick = { onClick(tab.id) }
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { selected === tab.id ? undefined : -1 }>
|
||||
{tab.label}
|
||||
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import React, { ReactNode, useRef } from 'react';
|
||||
import { keyframes } from 'styled-components';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { TEXT_OVERFLOW_TYPES } from '../../constants.web';
|
||||
|
||||
interface ITextWithOverflowProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
overflowType?: TEXT_OVERFLOW_TYPES;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<{ translateDiff: number; }>()((_, { translateDiff }) => {
|
||||
return {
|
||||
animation: {
|
||||
'&:hover': {
|
||||
animation: `${keyframes`
|
||||
0%, 20% {
|
||||
transform: translateX(0%);
|
||||
left: 0%;
|
||||
}
|
||||
80%, 100% {
|
||||
transform: translateX(-${translateDiff}px);
|
||||
left: 100%;
|
||||
}
|
||||
`} ${Math.max(translateDiff * 50, 2000)}ms infinite alternate linear;`
|
||||
}
|
||||
},
|
||||
textContainer: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
[TEXT_OVERFLOW_TYPES.ELLIPSIS]: {
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
[TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER]: {
|
||||
display: 'inline-block',
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const TextWithOverflow = ({
|
||||
className,
|
||||
overflowType = TEXT_OVERFLOW_TYPES.ELLIPSIS,
|
||||
children
|
||||
}: ITextWithOverflowProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLSpanElement>(null);
|
||||
const shouldAnimateOnHover = overflowType === TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER
|
||||
&& containerRef.current
|
||||
&& contentRef.current
|
||||
&& containerRef.current.clientWidth < contentRef.current.clientWidth;
|
||||
|
||||
const translateDiff = shouldAnimateOnHover ? contentRef.current.clientWidth - containerRef.current.clientWidth : 0;
|
||||
const { classes: styles, cx } = useStyles({ translateDiff });
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(className, styles.textContainer) }
|
||||
ref = { containerRef }>
|
||||
<span
|
||||
className = { cx(styles[overflowType], shouldAnimateOnHover && styles.animation) }
|
||||
ref = { contentRef }>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextWithOverflow;
|
|
@ -8,6 +8,14 @@ export enum BUTTON_TYPES {
|
|||
TERTIARY = 'tertiary'
|
||||
}
|
||||
|
||||
/**
|
||||
* Behaviour types for showing overflow text content.
|
||||
*/
|
||||
export enum TEXT_OVERFLOW_TYPES {
|
||||
ELLIPSIS = 'ellipsis',
|
||||
SCROLL_ON_HOVER = 'scroll-on-hover'
|
||||
}
|
||||
|
||||
/**
|
||||
* The modes of the buttons.
|
||||
*/
|
||||
|
|
|
@ -202,7 +202,8 @@ class CalendarList extends AbstractPage<Props> {
|
|||
className = 'meetings-list-empty-button'
|
||||
onClick = { this._onOpenSettings }
|
||||
onKeyPress = { this._onKeyPressOpenSettings }
|
||||
role = 'button'>
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Icon
|
||||
className = 'meetings-list-empty-icon'
|
||||
src = { IconCalendar } />
|
||||
|
|
|
@ -134,35 +134,38 @@ class Chat extends AbstractChat<Props> {
|
|||
_renderChat() {
|
||||
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
|
||||
|
||||
if (_isPollsTabFocused) {
|
||||
return (
|
||||
<>
|
||||
{ _isPollsEnabled && this._renderTabs() }
|
||||
<div
|
||||
aria-labelledby = { CHAT_TABS.POLLS }
|
||||
id = 'polls-panel'
|
||||
role = 'tabpanel'>
|
||||
<PollsPane />
|
||||
</div>
|
||||
<KeyboardAvoider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ _isPollsEnabled && this._renderTabs() }
|
||||
<div
|
||||
aria-labelledby = { CHAT_TABS.CHAT }
|
||||
className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
|
||||
id = 'chat-panel'
|
||||
role = 'tabpanel'>
|
||||
className = { clsx(
|
||||
'chat-panel',
|
||||
!_isPollsEnabled && 'chat-panel-no-tabs',
|
||||
_isPollsTabFocused && 'hide'
|
||||
) }
|
||||
id = { `${CHAT_TABS.CHAT}-panel` }
|
||||
role = 'tabpanel'
|
||||
tabIndex = { 0 }>
|
||||
<MessageContainer
|
||||
messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInput
|
||||
onSend = { this._onSendMessage } />
|
||||
</div>
|
||||
{ _isPollsEnabled && (
|
||||
<>
|
||||
<div
|
||||
aria-labelledby = { CHAT_TABS.POLLS }
|
||||
className = { clsx('polls-panel', !_isPollsTabFocused && 'hide') }
|
||||
id = { `${CHAT_TABS.POLLS}-panel` }
|
||||
role = 'tabpanel'
|
||||
tabIndex = { 0 }>
|
||||
<PollsPane />
|
||||
</div>
|
||||
<KeyboardAvoider />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -185,11 +188,13 @@ class Chat extends AbstractChat<Props> {
|
|||
accessibilityLabel: t('chat.tabs.chat'),
|
||||
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
|
||||
id: CHAT_TABS.CHAT,
|
||||
controlsId: `${CHAT_TABS.CHAT}-panel`,
|
||||
label: t('chat.tabs.chat')
|
||||
}, {
|
||||
accessibilityLabel: t('chat.tabs.polls'),
|
||||
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
|
||||
id: CHAT_TABS.POLLS,
|
||||
controlsId: `${CHAT_TABS.POLLS}-panel`,
|
||||
label: t('chat.tabs.polls')
|
||||
}
|
||||
] } />
|
||||
|
|
|
@ -115,7 +115,6 @@ class ChatInput extends Component<IProps, IState> {
|
|||
</div>
|
||||
)}
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = 'chat-input'
|
||||
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
|
||||
iconClick = { this._toggleSmileysPanel }
|
||||
|
|
|
@ -191,7 +191,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
|||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { selectedTab, selectedSource, sources } = this.state;
|
||||
const { selectedTab, selectedSource, sources, types } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -204,14 +204,27 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
|||
size = 'large'
|
||||
titleKey = 'dialog.shareYourScreen'>
|
||||
{ this._renderTabs() }
|
||||
<DesktopPickerPane
|
||||
key = { selectedTab }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onSubmit }
|
||||
onShareAudioChecked = { this._onShareAudioChecked }
|
||||
selectedSourceId = { selectedSource.id }
|
||||
sources = { sources[selectedTab as keyof typeof sources] }
|
||||
type = { selectedTab } />
|
||||
{types.map(type => (
|
||||
<div
|
||||
aria-labelledby = { `${type}-button` }
|
||||
className = { selectedTab === type ? undefined : 'hide' }
|
||||
id = { `${type}-panel` }
|
||||
key = { type }
|
||||
role = 'tabpanel'
|
||||
tabIndex = { 0 }>
|
||||
{selectedTab === type && (
|
||||
<DesktopPickerPane
|
||||
key = { selectedTab }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onSubmit }
|
||||
onShareAudioChecked = { this._onShareAudioChecked }
|
||||
selectedSourceId = { selectedSource.id }
|
||||
sources = { sources[selectedTab as keyof typeof sources] }
|
||||
type = { selectedTab } />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -348,17 +361,18 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
|||
type => {
|
||||
return {
|
||||
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
|
||||
id: type,
|
||||
id: `${type}-tab`,
|
||||
controlsId: `${type}-panel`,
|
||||
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
accessibilityLabel = ''
|
||||
accessibilityLabel = { t('dialog.sharingTabs') }
|
||||
className = 'desktop-picker-tabs-container'
|
||||
onChange = { this._onTabSelected }
|
||||
selected = { this.state.selectedTab }
|
||||
selected = { `${this.state.selectedTab}-tab` }
|
||||
tabs = { tabs } />);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,31 +7,23 @@ import {
|
|||
} from '../base/devices/actions';
|
||||
import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { toggleNoiseSuppression } from '../noise-suppression/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions';
|
||||
|
||||
import { getDeviceSelectionDialogProps } from './functions';
|
||||
import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Submits the settings related to device selection.
|
||||
* Submits the settings related to audio device selection.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId,
|
||||
userSelectedCameraDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
|
||||
}
|
||||
const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
|
||||
dispatch(updateSettings({
|
||||
|
@ -44,8 +36,8 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
|
|||
}
|
||||
|
||||
if (newState.selectedAudioOutputId
|
||||
&& newState.selectedAudioOutputId
|
||||
!== currentState.selectedAudioOutputId) {
|
||||
&& newState.selectedAudioOutputId
|
||||
!== currentState.selectedAudioOutputId) {
|
||||
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
|
||||
|
||||
setAudioOutputDeviceId(
|
||||
|
@ -62,5 +54,45 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
|
|||
err);
|
||||
});
|
||||
}
|
||||
|
||||
if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
|
||||
dispatch(toggleNoiseSuppression());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings related to device selection.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getVideoDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId,
|
||||
userSelectedCameraDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
|
||||
}
|
||||
|
||||
if (newState.localFlipX !== currentState.localFlipX) {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: newState.localFlipX
|
||||
}));
|
||||
}
|
||||
|
||||
if (newState.currentFramerate !== currentState.currentFramerate) {
|
||||
const frameRate = parseInt(newState.currentFramerate, 10);
|
||||
|
||||
dispatch(setScreenshareFramerate(frameRate));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type IProps as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
import logger from '../logger';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceHidContainer from './DeviceHidContainer.web';
|
||||
import DeviceSelector from './DeviceSelector.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: {
|
||||
audioInput?: MediaDeviceInfo[];
|
||||
audioOutput?: MediaDeviceInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not the audio selector can be interacted with. If true,
|
||||
* the audio input selector will be rendered as disabled. This is
|
||||
* specifically used to prevent audio device changing in Firefox, which
|
||||
* currently does not work due to a browser-side regression.
|
||||
*/
|
||||
disableAudioInputChange: boolean;
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasAudioPermission: boolean;
|
||||
|
||||
/**
|
||||
* If true, the audio meter will not display. Necessary for browsers or
|
||||
* configurations that do not support local stats to prevent a
|
||||
* non-responsive mic preview from displaying.
|
||||
*/
|
||||
hideAudioInputPreview: boolean;
|
||||
|
||||
/**
|
||||
* If true, the button to play a test sound on the selected speaker will not be displayed.
|
||||
* This needs to be hidden on browsers that do not support selecting an audio output device.
|
||||
*/
|
||||
hideAudioOutputPreview: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the audio output source selector should display. If
|
||||
* true, the audio output selector and test audio link will not be
|
||||
* rendered.
|
||||
*/
|
||||
hideAudioOutputSelect: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the hid device container should display.
|
||||
*/
|
||||
hideDeviceHIDContainer: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide noise suppression checkbox or not.
|
||||
*/
|
||||
hideNoiseSuppression: boolean;
|
||||
|
||||
/**
|
||||
* Wether noise suppression is on or not.
|
||||
*/
|
||||
noiseSuppressionEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
selectedAudioInputId: string;
|
||||
|
||||
/**
|
||||
* The id of the audio output device to preview.
|
||||
*/
|
||||
selectedAudioOutputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
previewAudioTrack?: any | null;
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '0 2px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
inputContainer: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
outputContainer: {
|
||||
margin: `${theme.spacing(5)} 0`,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
|
||||
outputButton: {
|
||||
marginLeft: theme.spacing(3)
|
||||
},
|
||||
|
||||
noiseSuppressionContainer: {
|
||||
marginBottom: theme.spacing(5)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AudioDevicesSelection extends AbstractDialogTab<IProps, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewAudioTrack: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => {
|
||||
this.props.dispatch(getAvailableDevices());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps.selectedAudioInputId
|
||||
!== this.props.selectedAudioInputId) {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeAudioInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
hasAudioPermission,
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputPreview,
|
||||
hideDeviceHIDContainer,
|
||||
hideNoiseSuppression,
|
||||
noiseSuppressionEnabled,
|
||||
selectedAudioOutputId,
|
||||
t
|
||||
} = this.props;
|
||||
const { audioInput, audioOutput } = this._getSelectors();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
aria-live = 'polite'
|
||||
className = { classes.inputContainer }>
|
||||
{this._renderSelector(audioInput)}
|
||||
</div>
|
||||
{!hideAudioInputPreview && hasAudioPermission
|
||||
&& <AudioInputPreview
|
||||
track = { this.state.previewAudioTrack } />}
|
||||
<div
|
||||
aria-live = 'polite'
|
||||
className = { classes.outputContainer }>
|
||||
{this._renderSelector(audioOutput)}
|
||||
{!hideAudioOutputPreview && hasAudioPermission
|
||||
&& <AudioOutputPreview
|
||||
className = { classes.outputButton }
|
||||
deviceId = { selectedAudioOutputId } />}
|
||||
</div>
|
||||
{!hideNoiseSuppression && (
|
||||
<div className = { classes.noiseSuppressionContainer }>
|
||||
<Checkbox
|
||||
checked = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.enableNoiseSuppression') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({
|
||||
noiseSuppressionEnabled: !noiseSuppressionEnabled
|
||||
}) } />
|
||||
</div>
|
||||
)}
|
||||
{!hideDeviceHIDContainer
|
||||
&& <DeviceHidContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitsiTrack for the audio input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of audio input device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId: string) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current audio input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeAudioInputPreview(): Promise<any> {
|
||||
return this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSelector(deviceSelectorProps: any) {
|
||||
return deviceSelectorProps ? (
|
||||
<DeviceSelector
|
||||
{ ...deviceSelectorProps }
|
||||
key = { deviceSelectorProps.id } />
|
||||
) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object configurations for audio input and output.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} Configurations.
|
||||
*/
|
||||
_getSelectors() {
|
||||
const { availableDevices, hasAudioPermission } = this.props;
|
||||
|
||||
const audioInput = {
|
||||
devices: availableDevices.audioInput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
id: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
};
|
||||
let audioOutput;
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
audioOutput = {
|
||||
devices: availableDevices.audioOutput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-speaker',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
id: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: (selectedAudioOutputId: string) => super._onChange({ selectedAudioOutputId }),
|
||||
selectedDeviceId: this.props.selectedAudioOutputId
|
||||
};
|
||||
}
|
||||
|
||||
return { audioInput,
|
||||
audioOutput };
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices ?? {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(translate(AudioDevicesSelection)));
|
|
@ -1,150 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to show an audio level meter for.
|
||||
*/
|
||||
track: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current audio input level being received, from 0 to 1.
|
||||
*/
|
||||
audioLevel: number
|
||||
};
|
||||
|
||||
/**
|
||||
* React component for displaying a audio level meter for a JitsiLocalTrack.
|
||||
*/
|
||||
class AudioInputPreview extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new AudioInputPreview instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioLevel: 0
|
||||
};
|
||||
|
||||
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates after the initial render.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._listenForAudioUpdates(this.props.track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for audio level updates on the old track and starts
|
||||
* listening instead on the new track.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.track !== this.props.track) {
|
||||
this._listenForAudioUpdates(this.props.track);
|
||||
this._updateAudioLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from audio level updates.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._stopListeningForAudioUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const audioMeterFill = {
|
||||
width: `${Math.floor(this.state.audioLevel * 100)}%`
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = 'audio-input-preview' >
|
||||
<div
|
||||
className = 'audio-input-preview-level'
|
||||
style = { audioMeterFill } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates from the library.
|
||||
*
|
||||
* @param {JitstiLocalTrack} track - The track to listen to for audio level
|
||||
* updates.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_listenForAudioUpdates(track) {
|
||||
this._stopListeningForAudioUpdates();
|
||||
|
||||
track && track.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateAudioLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to further updates from the current track.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopListeningForAudioUpdates() {
|
||||
this.props.track && this.props.track.off(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateAudioLevel);
|
||||
}
|
||||
|
||||
_updateAudioLevel: (number) => void;
|
||||
|
||||
/**
|
||||
* Updates the internal state of the last know audio level. The level should
|
||||
* be between 0 and 1, as the level will be used as a percentage out of 1.
|
||||
*
|
||||
* @param {number} audioLevel - The new audio level for the track.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAudioLevel(audioLevel) {
|
||||
this.setState({
|
||||
audioLevel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioInputPreview;
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
// @ts-ignore
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet/_.web';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to show an audio level meter for.
|
||||
*/
|
||||
track: any;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
section: {
|
||||
flex: 1,
|
||||
height: '4px',
|
||||
borderRadius: '1px',
|
||||
backgroundColor: theme.palette.ui04,
|
||||
marginRight: theme.spacing(1),
|
||||
|
||||
'&:last-of-type': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
|
||||
activeSection: {
|
||||
backgroundColor: theme.palette.success01
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const NO_OF_PREVIEW_SECTIONS = 11;
|
||||
|
||||
const AudioInputPreview = (props: IProps) => {
|
||||
const [ audioLevel, setAudioLevel ] = useState(0);
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates from the library.
|
||||
*
|
||||
* @param {JitsiLocalTrack} track - The track to listen to for audio level
|
||||
* updates.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _listenForAudioUpdates(track: any) {
|
||||
_stopListeningForAudioUpdates();
|
||||
|
||||
track?.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
setAudioLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to further updates from the current track.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _stopListeningForAudioUpdates() {
|
||||
props.track?.off(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
setAudioLevel);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
_listenForAudioUpdates(props.track);
|
||||
|
||||
return _stopListeningForAudioUpdates;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
_listenForAudioUpdates(props.track);
|
||||
setAudioLevel(0);
|
||||
}, [ props.track ]);
|
||||
|
||||
const audioMeterFill = Math.ceil(Math.floor(audioLevel * 100) / (100 / NO_OF_PREVIEW_SECTIONS));
|
||||
|
||||
return (
|
||||
<div className = { classes.container } >
|
||||
{new Array(NO_OF_PREVIEW_SECTIONS).fill(0)
|
||||
.map((_, idx) =>
|
||||
(<div
|
||||
className = { cx(classes.section, idx < audioMeterFill && classes.activeSection) }
|
||||
key = { idx } />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioInputPreview;
|
|
@ -1,35 +1,38 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Audio from '../../base/media/components/Audio';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import Audio from '../../base/media/components/Audio.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.any';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.mp3';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioOutputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Button className.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The device id of the audio output device to use.
|
||||
*/
|
||||
deviceId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component for playing a test sound through a specified audio device.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AudioOutputPreview extends Component<Props> {
|
||||
_audioElement: ?Object;
|
||||
class AudioOutputPreview extends Component<IProps> {
|
||||
_audioElement: HTMLAudioElement | null;
|
||||
|
||||
/**
|
||||
* Initializes a new AudioOutputPreview instance.
|
||||
|
@ -37,7 +40,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._audioElement = null;
|
||||
|
@ -66,24 +69,21 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = 'audio-output-preview'>
|
||||
<a
|
||||
aria-label = { this.props.t('deviceSelection.testAudio') }
|
||||
<>
|
||||
<Button
|
||||
accessibilityLabel = { this.props.t('deviceSelection.testAudio') }
|
||||
className = { this.props.className }
|
||||
labelKey = 'deviceSelection.testAudio'
|
||||
onClick = { this._onClick }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ this.props.t('deviceSelection.testAudio') }
|
||||
</a>
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
<Audio
|
||||
setRef = { this._audioElementReady }
|
||||
src = { TEST_SOUND_PATH } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
_audioElementReady: (Object) => void;
|
||||
|
||||
/**
|
||||
* Sets the instance variable for the component's audio element so it can be
|
||||
* accessed directly.
|
||||
|
@ -92,14 +92,12 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_audioElementReady(element: Object) {
|
||||
_audioElementReady(element: HTMLAudioElement) {
|
||||
this._audioElement = element;
|
||||
|
||||
this._setAudioSink();
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* Plays a test sound.
|
||||
*
|
||||
|
@ -107,12 +105,9 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._audioElement
|
||||
&& this._audioElement.play();
|
||||
this._audioElement?.play();
|
||||
}
|
||||
|
||||
_onKeyPress: (Object) => void;
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
|
@ -120,7 +115,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e) {
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClick();
|
||||
|
@ -135,7 +130,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*/
|
||||
_setAudioSink() {
|
||||
this._audioElement
|
||||
&& this.props.deviceId
|
||||
&& this.props.deviceId // @ts-ignore
|
||||
&& this._audioElement.setSinkId(this.props.deviceId);
|
||||
}
|
||||
}
|
|
@ -5,33 +5,40 @@ import { makeStyles } from 'tss-react/mui';
|
|||
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconTrash } from '../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.any';
|
||||
import { closeHidDevice, requestHidDevice } from '../../web-hid/actions';
|
||||
import { getDeviceInfo, shouldRequestHIDDevice } from '../../web-hid/functions';
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
callControlContainer: {
|
||||
marginTop: '8px',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
'> label': {
|
||||
display: 'block',
|
||||
marginBottom: '20px'
|
||||
}
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
|
||||
label: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
|
||||
deviceRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
deleteDevice: {
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
headerConnectedDevice: {
|
||||
fontWeight: 600
|
||||
},
|
||||
|
||||
hidContainer: {
|
||||
'> span': {
|
||||
marginLeft: '16px'
|
||||
|
@ -66,7 +73,7 @@ function DeviceHidContainer() {
|
|||
className = { classes.callControlContainer }
|
||||
key = 'callControl'>
|
||||
<label
|
||||
className = 'device-selector-label'
|
||||
className = { classes.label }
|
||||
htmlFor = 'callControl'>
|
||||
{t('deviceSelection.hid.callControl')}
|
||||
</label>
|
||||
|
@ -77,7 +84,6 @@ function DeviceHidContainer() {
|
|||
key = 'request-control-btn'
|
||||
label = { t('deviceSelection.hid.pairDevice') }
|
||||
onClick = { onRequestControl }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
)}
|
||||
{!showRequestDeviceInfo && (
|
||||
|
|
|
@ -1,429 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type Props as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceHidContainer from './DeviceHidContainer.web';
|
||||
import DeviceSelector from './DeviceSelector';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DeviceSelection}.
|
||||
*/
|
||||
export type Props = {
|
||||
...$Exact<AbstractDialogTabProps>,
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: Object,
|
||||
|
||||
/**
|
||||
* Whether or not the audio selector can be interacted with. If true,
|
||||
* the audio input selector will be rendered as disabled. This is
|
||||
* specifically used to prevent audio device changing in Firefox, which
|
||||
* currently does not work due to a browser-side regression.
|
||||
*/
|
||||
disableAudioInputChange: boolean,
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean,
|
||||
|
||||
/**
|
||||
* Whether video input dropdown should be enabled or not.
|
||||
*/
|
||||
disableVideoInputSelect: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasAudioPermission: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasVideoPermission: boolean,
|
||||
|
||||
/**
|
||||
* If true, the audio meter will not display. Necessary for browsers or
|
||||
* configurations that do not support local stats to prevent a
|
||||
* non-responsive mic preview from displaying.
|
||||
*/
|
||||
hideAudioInputPreview: boolean,
|
||||
|
||||
/**
|
||||
* If true, the button to play a test sound on the selected speaker will not be displayed.
|
||||
* This needs to be hidden on browsers that do not support selecting an audio output device.
|
||||
*/
|
||||
hideAudioOutputPreview: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio output source selector should display. If
|
||||
* true, the audio output selector and test audio link will not be
|
||||
* rendered.
|
||||
*/
|
||||
hideAudioOutputSelect: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the hid device container should display.
|
||||
*/
|
||||
hideDeviceHIDContainer: boolean,
|
||||
|
||||
/**
|
||||
* Whether video input preview should be displayed or not.
|
||||
* (In the case of iOS Safari).
|
||||
*/
|
||||
hideVideoInputPreview: boolean,
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
selectedAudioInputId: string,
|
||||
|
||||
/**
|
||||
* The id of the audio output device to preview.
|
||||
*/
|
||||
selectedAudioOutputId: string,
|
||||
|
||||
/**
|
||||
* The id of the video input device to preview.
|
||||
*/
|
||||
selectedVideoInputId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link DeviceSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
previewAudioTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing video input.
|
||||
*/
|
||||
previewVideoTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The error message from trying to use a video input device.
|
||||
*/
|
||||
previewVideoTrackError: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewAudioTrack: null,
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId),
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => getAvailableDevices());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedAudioInputId
|
||||
!== this.props.selectedAudioInputId) {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
}
|
||||
|
||||
if (prevProps.selectedVideoInputId
|
||||
!== this.props.selectedVideoInputId) {
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeAudioInputPreview();
|
||||
this._disposeVideoInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputPreview,
|
||||
hideDeviceHIDContainer,
|
||||
hideVideoInputPreview,
|
||||
selectedAudioOutputId
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
|
||||
<div className = 'device-selection-column column-video'>
|
||||
{ !hideVideoInputPreview
|
||||
&& <div className = 'device-selection-video-container'>
|
||||
<VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
</div>
|
||||
}
|
||||
{ !hideAudioInputPreview
|
||||
&& <AudioInputPreview
|
||||
track = { this.state.previewAudioTrack } /> }
|
||||
</div>
|
||||
<div className = 'device-selection-column column-selectors'>
|
||||
<div
|
||||
aria-live = 'polite all'
|
||||
className = 'device-selectors'>
|
||||
{ this._renderSelectors() }
|
||||
</div>
|
||||
{ !hideAudioOutputPreview
|
||||
&& <AudioOutputPreview
|
||||
deviceId = { selectedAudioOutputId } /> }
|
||||
{ !hideDeviceHIDContainer
|
||||
&& <DeviceHidContainer /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitiTrack for the audio input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of audio input device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitiTrack for the video input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of video device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createVideoInputTrack(deviceId) {
|
||||
const { hideVideoInputPreview } = this.props;
|
||||
|
||||
if (hideVideoInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current audio input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeAudioInputPreview(): Promise<*> {
|
||||
return this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current video input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeVideoInputPreview(): Promise<*> {
|
||||
return this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSelector(deviceSelectorProps) {
|
||||
return (
|
||||
<div key = { deviceSelectorProps.label }>
|
||||
<label
|
||||
className = 'device-selector-label'
|
||||
htmlFor = { deviceSelectorProps.id }>
|
||||
{ this.props.t(deviceSelectorProps.label) }
|
||||
</label>
|
||||
<DeviceSelector { ...deviceSelectorProps } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DeviceSelector instances for video output, audio input, and audio
|
||||
* output.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<ReactElement>} DeviceSelector instances.
|
||||
*/
|
||||
_renderSelectors() {
|
||||
const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
|
||||
|
||||
const configurations = [
|
||||
{
|
||||
devices: availableDevices.audioInput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
id: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
},
|
||||
{
|
||||
devices: availableDevices.videoInput,
|
||||
hasPermission: hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
id: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
|
||||
selectedDeviceId: this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
configurations.push({
|
||||
devices: availableDevices.audioOutput,
|
||||
hasPermission: hasAudioPermission || hasVideoPermission,
|
||||
icon: 'icon-speaker',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
id: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
|
||||
selectedDeviceId: this.props.selectedAudioOutputId
|
||||
});
|
||||
}
|
||||
|
||||
return configurations.map(config => this._renderSelector(config));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(DeviceSelection);
|
|
@ -1,58 +1,76 @@
|
|||
/* @flow */
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Select from '../../base/ui/components/web/Select';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DeviceSelector}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* MediaDeviceInfos used for display in the select element.
|
||||
*/
|
||||
devices: Array<Object>,
|
||||
devices: Array<MediaDeviceInfo> | undefined;
|
||||
|
||||
/**
|
||||
* If false, will return a selector with no selection options.
|
||||
*/
|
||||
hasPermission: boolean,
|
||||
hasPermission: boolean;
|
||||
|
||||
/**
|
||||
* CSS class for the icon to the left of the dropdown trigger.
|
||||
*/
|
||||
icon: string,
|
||||
|
||||
/**
|
||||
* If true, will render the selector disabled with a default selection.
|
||||
*/
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The translation key to display as a menu label.
|
||||
*/
|
||||
label: string,
|
||||
|
||||
/**
|
||||
* The callback to invoke when a selection is made.
|
||||
*/
|
||||
onSelect: Function,
|
||||
|
||||
/**
|
||||
* The default device to display as selected.
|
||||
*/
|
||||
selectedDeviceId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* The id of the dropdown element.
|
||||
*/
|
||||
id: string
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* If true, will render the selector disabled with a default selection.
|
||||
*/
|
||||
isDisabled: boolean;
|
||||
|
||||
/**
|
||||
* The translation key to display as a menu label.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The callback to invoke when a selection is made.
|
||||
*/
|
||||
onSelect: Function;
|
||||
|
||||
/**
|
||||
* The default device to display as selected.
|
||||
*/
|
||||
selectedDeviceId: string;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
textSelector: {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
padding: '10px 16px',
|
||||
textAlign: 'center',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
border: `1px solid ${theme.palette.ui03}`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -61,17 +79,18 @@ type Props = {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DeviceSelector extends Component<Props> {
|
||||
class DeviceSelector extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new DeviceSelector instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onSelect = this._onSelect.bind(this);
|
||||
this._createDropdown = this._createDropdown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,25 +130,6 @@ class DeviceSelector extends Component<Props> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a React Element for displaying the passed in text surrounded by
|
||||
* two icons. The left icon is the icon class passed in through props and
|
||||
* the right icon is AtlasKit ExpandIcon.
|
||||
*
|
||||
* @param {string} triggerText - The text to display within the element.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdownTrigger(triggerText) {
|
||||
return (
|
||||
<div className = 'device-selector-trigger'>
|
||||
<span className = 'device-selector-trigger-text'>
|
||||
{ triggerText }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a AKDropdownMenu Component using passed in props and options. If
|
||||
* the dropdown needs to be disabled, then only the AKDropdownMenu trigger
|
||||
|
@ -146,32 +146,30 @@ class DeviceSelector extends Component<Props> {
|
|||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdown(options) {
|
||||
_createDropdown(options: { defaultSelected?: MediaDeviceInfo; isDisabled: boolean;
|
||||
items?: Array<{ label: string; value: string; }>; placeholder: string; }) {
|
||||
const triggerText
|
||||
= (options.defaultSelected && (options.defaultSelected.label || options.defaultSelected.deviceId))
|
||||
|| options.placeholder;
|
||||
const trigger = this._createDropdownTrigger(triggerText);
|
||||
const { classes } = this.props;
|
||||
|
||||
if (options.isDisabled || !options.items.length) {
|
||||
if (options.isDisabled || !options.items?.length) {
|
||||
return (
|
||||
<div className = 'device-selector-trigger-disabled'>
|
||||
{ trigger }
|
||||
<div className = { classes.textSelector }>
|
||||
{triggerText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
onChange = { this._onSelect }
|
||||
options = { options.items }
|
||||
value = { this.props.selectedDeviceId } />
|
||||
</div>
|
||||
<Select
|
||||
label = { this.props.t(this.props.label) }
|
||||
onChange = { this._onSelect }
|
||||
options = { options.items }
|
||||
value = { this.props.selectedDeviceId } />
|
||||
);
|
||||
}
|
||||
|
||||
_onSelect: (Object) => void;
|
||||
|
||||
/**
|
||||
* Invokes the passed in callback to notify of selection changes.
|
||||
*
|
||||
|
@ -180,7 +178,7 @@ class DeviceSelector extends Component<Props> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelect(e) {
|
||||
_onSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const deviceId = e.target.value;
|
||||
|
||||
if (this.props.selectedDeviceId !== deviceId) {
|
||||
|
@ -217,4 +215,4 @@ class DeviceSelector extends Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default translate(DeviceSelector);
|
||||
export default withStyles(styles)(translate(DeviceSelector));
|
|
@ -0,0 +1,368 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type IProps as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
import Select from '../../base/ui/components/web/Select';
|
||||
import { SS_DEFAULT_FRAME_RATE } from '../../settings/constants';
|
||||
import logger from '../logger';
|
||||
|
||||
import DeviceSelector from './DeviceSelector.web';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoDeviceSelection}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: { videoInput?: MediaDeviceInfo[]; };
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* The currently selected desktop share frame rate in the frame rate select dropdown.
|
||||
*/
|
||||
currentFramerate: string;
|
||||
|
||||
/**
|
||||
* All available desktop capture frame rates.
|
||||
*/
|
||||
desktopShareFramerates: Array<number>;
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean;
|
||||
|
||||
/**
|
||||
* Whether video input dropdown should be enabled or not.
|
||||
*/
|
||||
disableVideoInputSelect: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasVideoPermission: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide the additional settings or not.
|
||||
*/
|
||||
hideAdditionalSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether video input preview should be displayed or not.
|
||||
* (In the case of iOS Safari).
|
||||
*/
|
||||
hideVideoInputPreview: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local video is flipped.
|
||||
*/
|
||||
localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* The id of the video input device to preview.
|
||||
*/
|
||||
selectedVideoInputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VideoDeviceSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing video input.
|
||||
*/
|
||||
previewVideoTrack: any | null;
|
||||
|
||||
/**
|
||||
* The error message from trying to use a video input device.
|
||||
*/
|
||||
previewVideoTrackError: string | null;
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '0 2px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
checkboxContainer: {
|
||||
margin: `${theme.spacing(4)} 0`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoDeviceSelection extends AbstractDialogTab<IProps, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
|
||||
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => {
|
||||
this.props.dispatch(getAvailableDevices());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
|
||||
if (prevProps.selectedVideoInputId
|
||||
!== this.props.selectedVideoInputId) {
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeVideoInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
hideAdditionalSettings,
|
||||
hideVideoInputPreview,
|
||||
localFlipX,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{ !hideVideoInputPreview
|
||||
&& <VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
localFlipX = { localFlipX }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
}
|
||||
<div
|
||||
aria-live = 'polite'>
|
||||
{this._renderVideoSelector()}
|
||||
</div>
|
||||
{!hideAdditionalSettings && (
|
||||
<>
|
||||
<div className = { classes.checkboxContainer }>
|
||||
<Checkbox
|
||||
checked = { localFlipX }
|
||||
label = { t('videothumbnail.mirrorVideo') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({ localFlipX: !localFlipX }) } />
|
||||
</div>
|
||||
{this._renderFramerateSelect()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitsiTrack for the video input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of video device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createVideoInputTrack(deviceId: string) {
|
||||
const { hideVideoInputPreview } = this.props;
|
||||
|
||||
if (hideVideoInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current video input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeVideoInputPreview(): Promise<any> {
|
||||
return this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoSelector() {
|
||||
const { availableDevices, hasVideoPermission } = this.props;
|
||||
|
||||
const videoConfig = {
|
||||
devices: availableDevices.videoInput,
|
||||
hasPermission: hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
id: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: (selectedVideoInputId: string) => super._onChange({ selectedVideoInputId }),
|
||||
selectedDeviceId: this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
|
||||
};
|
||||
|
||||
return (
|
||||
<DeviceSelector
|
||||
{ ...videoConfig }
|
||||
key = { videoConfig.id } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a frame rate from the select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const frameRate = e.target.value;
|
||||
|
||||
super._onChange({ currentFramerate: frameRate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for the desktop share frame rate dropdown.
|
||||
*
|
||||
* @returns {JSX}
|
||||
*/
|
||||
_renderFramerateSelect() {
|
||||
const { currentFramerate, desktopShareFramerates, t } = this.props;
|
||||
const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
|
||||
return {
|
||||
value: frameRate,
|
||||
label: `${frameRate} ${t('settings.framesPerSecond')}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
|
||||
? t('settings.desktopShareHighFpsWarning')
|
||||
: t('settings.desktopShareWarning') }
|
||||
label = { t('settings.desktopShareFramerate') }
|
||||
onChange = { this._onFramerateItemSelect }
|
||||
options = { frameRateItems }
|
||||
value = { currentFramerate } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices ?? {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(translate(VideoDeviceSelection)));
|
|
@ -1,58 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Video from '../../base/media/components/Video';
|
||||
|
||||
const VIDEO_ERROR_CLASS = 'video-preview-has-error';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoInputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* An error message to display instead of a preview. Displaying an error
|
||||
* will take priority over displaying a video preview.
|
||||
*/
|
||||
error: ?string,
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
track: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* React component for displaying video. This component defers to lib-jitsi-meet
|
||||
* logic for rendering the video.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoInputPreview extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { error } = this.props;
|
||||
const errorClass = error ? VIDEO_ERROR_CLASS : '';
|
||||
const className = `video-input-preview ${errorClass}`;
|
||||
|
||||
return (
|
||||
<div className = { className }>
|
||||
<Video
|
||||
className = 'video-input-preview-display flipVideoX'
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: this.props.track }} />
|
||||
<div className = 'video-input-preview-error'>
|
||||
{ error || '' }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoInputPreview;
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Video from '../../base/media/components/Video.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoInputPreview}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* An error message to display instead of a preview. Displaying an error
|
||||
* will take priority over displaying a video preview.
|
||||
*/
|
||||
error: string | null;
|
||||
|
||||
/**
|
||||
* Whether or not the local video is flipped.
|
||||
*/
|
||||
localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
track: Object;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
marginBottom: theme.spacing(4),
|
||||
backgroundColor: theme.palette.uiBackground
|
||||
},
|
||||
|
||||
video: {
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
errorText: {
|
||||
color: theme.palette.text01,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
top: '50%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const VideoInputPreview = ({ error, localFlipX, track }: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<Video
|
||||
className = { cx(classes.video, localFlipX && 'flipVideoX') }
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: track }} />
|
||||
{error && (
|
||||
<div className = { classes.errorText }>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoInputPreview;
|
|
@ -1,4 +0,0 @@
|
|||
// @flow
|
||||
|
||||
export { default as DeviceSelection } from './DeviceSelection';
|
||||
export type { Props as DeviceSelectionProps } from './DeviceSelection';
|
|
@ -21,8 +21,69 @@ import {
|
|||
getUserSelectedMicDeviceId,
|
||||
getUserSelectedOutputDeviceId
|
||||
} from '../base/settings/functions.web';
|
||||
import { isNoiseSuppressionEnabled } from '../noise-suppression/functions';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from '../settings/constants';
|
||||
import { isDeviceHidSupported } from '../web-hid/functions';
|
||||
|
||||
/**
|
||||
* Returns the properties for the audio device selection dialog from Redux state.
|
||||
*
|
||||
* @param {IStateful} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the audio device selection dialog.
|
||||
*/
|
||||
export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
|
||||
// by the browser when a new track is created for preview. That's why we are disabling all previews.
|
||||
const disablePreviews = isIosMobileBrowser();
|
||||
|
||||
const state = toState(stateful);
|
||||
const settings = state['features/base/settings'];
|
||||
const { permissions } = state['features/base/devices'];
|
||||
const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
|
||||
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
|
||||
const userSelectedMic = getUserSelectedMicDeviceId(state);
|
||||
const deviceHidSupported = isDeviceHidSupported();
|
||||
const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
|
||||
const hideNoiseSuppression = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
|
||||
|
||||
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
|
||||
// case for Safari on iOS.
|
||||
let disableAudioInputChange
|
||||
= !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
|
||||
let selectedAudioInputId = settings.micDeviceId;
|
||||
let selectedAudioOutputId = getAudioOutputDeviceId();
|
||||
|
||||
// audio input change will be a problem only when we are in a
|
||||
// conference and this is not supported, when we open device selection on
|
||||
// welcome page changing input devices will not be a problem
|
||||
// on welcome page we also show only what we have saved as user selected devices
|
||||
if (isDisplayedOnWelcomePage) {
|
||||
disableAudioInputChange = false;
|
||||
selectedAudioInputId = userSelectedMic;
|
||||
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
|
||||
}
|
||||
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
// used or if none are currently used with what we have in settings(user selected)
|
||||
return {
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
hasAudioPermission: permissions.audio,
|
||||
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
|
||||
hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
|
||||
hideAudioOutputSelect: !speakerChangeSupported,
|
||||
hideDeviceHIDContainer: !deviceHidSupported,
|
||||
hideNoiseSuppression,
|
||||
noiseSuppressionEnabled,
|
||||
selectedAudioInputId,
|
||||
selectedAudioOutputId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the device selection dialog from Redux state.
|
||||
*
|
||||
|
@ -32,7 +93,7 @@ import { isDeviceHidSupported } from '../web-hid/functions';
|
|||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the device selection dialog.
|
||||
*/
|
||||
export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
|
||||
// by the browser when a new track is created for preview. That's why we are disabling all previews.
|
||||
const disablePreviews = isIosMobileBrowser();
|
||||
|
@ -41,18 +102,12 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
|
|||
const settings = state['features/base/settings'];
|
||||
const { permissions } = state['features/base/devices'];
|
||||
const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
|
||||
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
|
||||
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
|
||||
const userSelectedMic = getUserSelectedMicDeviceId(state);
|
||||
const deviceHidSupported = isDeviceHidSupported();
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const hideAdditionalSettings = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
|
||||
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
|
||||
|
||||
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
|
||||
// case for Safari on iOS.
|
||||
let disableAudioInputChange
|
||||
= !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
|
||||
let disableVideoInputSelect = !inputDeviceChangeSupported;
|
||||
let selectedAudioInputId = settings.micDeviceId;
|
||||
let selectedAudioOutputId = getAudioOutputDeviceId();
|
||||
let selectedVideoInputId = settings.cameraDeviceId;
|
||||
|
||||
// audio input change will be a problem only when we are in a
|
||||
|
@ -60,29 +115,21 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
|
|||
// welcome page changing input devices will not be a problem
|
||||
// on welcome page we also show only what we have saved as user selected devices
|
||||
if (isDisplayedOnWelcomePage) {
|
||||
disableAudioInputChange = false;
|
||||
disableVideoInputSelect = false;
|
||||
selectedAudioInputId = userSelectedMic;
|
||||
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
|
||||
selectedVideoInputId = userSelectedCamera;
|
||||
}
|
||||
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
// used or if none are currently used with what we have in settings(user selected)
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices,
|
||||
disableAudioInputChange,
|
||||
currentFramerate: framerate,
|
||||
desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
|
||||
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
disableVideoInputSelect,
|
||||
hasAudioPermission: permissions.audio,
|
||||
hasVideoPermission: permissions.video,
|
||||
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
|
||||
hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
|
||||
hideAudioOutputSelect: !speakerChangeSupported,
|
||||
hideDeviceHIDContainer: !deviceHidSupported,
|
||||
hideAdditionalSettings,
|
||||
hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
|
||||
selectedAudioInputId,
|
||||
selectedAudioOutputId,
|
||||
localFlipX: Boolean(localFlipX),
|
||||
selectedVideoInputId
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,25 +7,6 @@
|
|||
*/
|
||||
export const TOGGLE_E2EE = 'TOGGLE_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value whether everyone has E2EE enabled.
|
||||
*
|
||||
* {
|
||||
* type: SET_EVERYONE_ENABLED_E2EE,
|
||||
* everyoneEnabledE2EE: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value whether everyone supports E2EE.
|
||||
*
|
||||
* {
|
||||
* type: SET_EVERYONE_SUPPORT_E2EE
|
||||
* }
|
||||
*/
|
||||
export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value E2EE maxMode.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import {
|
||||
PARTICIPANT_VERIFIED,
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
SET_MEDIA_ENCRYPTION_KEY,
|
||||
START_VERIFICATION,
|
||||
|
@ -20,38 +18,6 @@ export function toggleE2EE(enabled: boolean) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new value whether everyone has E2EE enabled.
|
||||
*
|
||||
* @param {boolean} everyoneEnabledE2EE - The new value.
|
||||
* @returns {{
|
||||
* type: SET_EVERYONE_ENABLED_E2EE,
|
||||
* everyoneEnabledE2EE: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setEveryoneEnabledE2EE(everyoneEnabledE2EE: boolean) {
|
||||
return {
|
||||
type: SET_EVERYONE_ENABLED_E2EE,
|
||||
everyoneEnabledE2EE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new value whether everyone support E2EE.
|
||||
*
|
||||
* @param {boolean} everyoneSupportE2EE - The new value.
|
||||
* @returns {{
|
||||
* type: SET_EVERYONE_SUPPORT_E2EE,
|
||||
* everyoneSupportE2EE: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) {
|
||||
return {
|
||||
type: SET_EVERYONE_SUPPORT_E2EE,
|
||||
everyoneSupportE2EE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set E2EE maxMode.
|
||||
*
|
||||
|
|
|
@ -27,6 +27,6 @@ export function _mapStateToProps(state: IReduxState) {
|
|||
|
||||
return {
|
||||
_e2eeLabels: e2ee.labels,
|
||||
_showLabel: state['features/e2ee'].everyoneEnabledE2EE
|
||||
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getParticipantById, getParticipantCount } from '../base/participants/functions';
|
||||
import { getParticipantById, getParticipantCount, getParticipantCountWithFake } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
|
||||
|
@ -19,17 +19,17 @@ import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants';
|
|||
*/
|
||||
export function doesEveryoneSupportE2EE(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { everyoneSupportE2EE } = state['features/e2ee'];
|
||||
const { numberOfParticipantsNotSupportingE2EE } = state['features/base/participants'];
|
||||
const { e2eeSupported } = state['features/base/conference'];
|
||||
const participantCount = getParticipantCount(state);
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
|
||||
if (typeof everyoneSupportE2EE === 'undefined' && participantCount === 1) {
|
||||
if (participantCount === 1) {
|
||||
// This will happen if we are alone.
|
||||
|
||||
return e2eeSupported;
|
||||
}
|
||||
|
||||
return everyoneSupportE2EE;
|
||||
return numberOfParticipantsNotSupportingE2EE === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
|
@ -6,13 +5,11 @@ import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
|||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantUpdated } from '../base/participants/actions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getRemoteParticipants,
|
||||
isScreenShareParticipant
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
@ -20,7 +17,7 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
|||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
|
||||
import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
|
||||
import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
|
||||
import { setE2EEMaxMode, toggleE2EE } from './actions';
|
||||
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
|
||||
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
|
||||
import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
|
||||
|
@ -58,137 +55,24 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||
|
||||
break;
|
||||
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { id, e2eeEnabled, e2eeSupported } = action.participant;
|
||||
const oldParticipant = getParticipantById(getState(), id);
|
||||
const result = next(action);
|
||||
|
||||
if (e2eeEnabled !== oldParticipant?.e2eeEnabled
|
||||
|| e2eeSupported !== oldParticipant?.e2eeSupported) {
|
||||
const state = getState();
|
||||
let newEveryoneSupportE2EE = true;
|
||||
let newEveryoneEnabledE2EE = true;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ key, p ] of getRemoteParticipants(state)) {
|
||||
if (!p.e2eeEnabled) {
|
||||
newEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
if (!p.e2eeSupported) {
|
||||
newEveryoneSupportE2EE = false;
|
||||
}
|
||||
|
||||
if (!newEveryoneEnabledE2EE && !newEveryoneSupportE2EE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getLocalParticipant(state)?.e2eeEnabled) {
|
||||
newEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(setEveryoneEnabledE2EE(newEveryoneEnabledE2EE));
|
||||
dispatch(setEveryoneSupportE2EE(newEveryoneSupportE2EE));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
const result = next(action);
|
||||
const { e2eeEnabled, e2eeSupported, local } = action.participant;
|
||||
const { everyoneEnabledE2EE } = getState()['features/e2ee'];
|
||||
const participantCount = getParticipantCount(getState);
|
||||
|
||||
if (isScreenShareParticipant(action.participant)) {
|
||||
return result;
|
||||
if (!isScreenShareParticipant(action.participant) && !action.participant.local) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
// the initial values
|
||||
if (participantCount === 1) {
|
||||
batch(() => {
|
||||
dispatch(setEveryoneEnabledE2EE(e2eeEnabled));
|
||||
dispatch(setEveryoneSupportE2EE(e2eeSupported));
|
||||
});
|
||||
}
|
||||
|
||||
// if all had it enabled and this one disabled it, change value in store
|
||||
// otherwise there is no change in the value we store
|
||||
if (everyoneEnabledE2EE && !e2eeEnabled) {
|
||||
dispatch(setEveryoneEnabledE2EE(false));
|
||||
}
|
||||
|
||||
if (local) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { everyoneSupportE2EE } = getState()['features/e2ee'];
|
||||
|
||||
// if all supported it and this one does not, change value in store
|
||||
// otherwise there is no change in the value we store
|
||||
if (everyoneSupportE2EE && !e2eeSupported) {
|
||||
dispatch(setEveryoneSupportE2EE(false));
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const previosState = getState();
|
||||
const participant = getParticipantById(previosState, action.participant?.id);
|
||||
const participant = getParticipantById(getState(), action.participant?.id);
|
||||
const result = next(action);
|
||||
const newState = getState();
|
||||
const { e2eeEnabled = false, e2eeSupported = false } = participant ?? {};
|
||||
|
||||
if (isScreenShareParticipant(participant)) {
|
||||
return result;
|
||||
if (!isScreenShareParticipant(participant)) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
const { everyoneEnabledE2EE, everyoneSupportE2EE } = newState['features/e2ee'];
|
||||
|
||||
|
||||
// if it was not enabled by everyone, and the participant leaving had it disabled, or if it was not supported
|
||||
// by everyone, and the participant leaving had it not supported let's check is it enabled for all that stay
|
||||
if ((!everyoneEnabledE2EE && !e2eeEnabled) || (!everyoneSupportE2EE && !e2eeSupported)) {
|
||||
let latestEveryoneEnabledE2EE = true;
|
||||
let latestEveryoneSupportE2EE = true;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ key, p ] of getRemoteParticipants(newState)) {
|
||||
if (!p.e2eeEnabled) {
|
||||
latestEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
if (!p.e2eeSupported) {
|
||||
latestEveryoneSupportE2EE = false;
|
||||
}
|
||||
|
||||
if (!latestEveryoneEnabledE2EE && !latestEveryoneSupportE2EE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getLocalParticipant(newState)?.e2eeEnabled) {
|
||||
latestEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
if (!everyoneEnabledE2EE && latestEveryoneEnabledE2EE) {
|
||||
dispatch(setEveryoneEnabledE2EE(true));
|
||||
}
|
||||
|
||||
if (!everyoneSupportE2EE && latestEveryoneSupportE2EE) {
|
||||
dispatch(setEveryoneSupportE2EE(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -314,12 +198,23 @@ function _updateMaxMode(dispatch: IStore['dispatch'], getState: IStore['getState
|
|||
return;
|
||||
}
|
||||
|
||||
if (isMaxModeThresholdReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED));
|
||||
dispatch(toggleE2EE(false));
|
||||
const { maxMode, enabled } = state['features/e2ee'];
|
||||
const isMaxModeThresholdReachedValue = isMaxModeThresholdReached(state);
|
||||
let newMaxMode: string;
|
||||
|
||||
if (isMaxModeThresholdReachedValue) {
|
||||
newMaxMode = MAX_MODE.THRESHOLD_EXCEEDED;
|
||||
} else if (isMaxModeReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.ENABLED));
|
||||
newMaxMode = MAX_MODE.ENABLED;
|
||||
} else {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.DISABLED));
|
||||
newMaxMode = MAX_MODE.DISABLED;
|
||||
}
|
||||
|
||||
if (maxMode !== newMaxMode) {
|
||||
dispatch(setE2EEMaxMode(newMaxMode));
|
||||
}
|
||||
|
||||
if (isMaxModeThresholdReachedValue && !enabled) {
|
||||
dispatch(toggleE2EE(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
TOGGLE_E2EE
|
||||
} from './actionTypes';
|
||||
|
@ -15,8 +13,6 @@ const DEFAULT_STATE = {
|
|||
|
||||
export interface IE2EEState {
|
||||
enabled: boolean;
|
||||
everyoneEnabledE2EE?: boolean;
|
||||
everyoneSupportE2EE?: boolean;
|
||||
maxMode: string;
|
||||
}
|
||||
|
||||
|
@ -34,16 +30,6 @@ ReducerRegistry.register<IE2EEState>('features/e2ee', (state = DEFAULT_STATE, ac
|
|||
...state,
|
||||
enabled: action.enabled
|
||||
};
|
||||
case SET_EVERYONE_ENABLED_E2EE:
|
||||
return {
|
||||
...state,
|
||||
everyoneEnabledE2EE: action.everyoneEnabledE2EE
|
||||
};
|
||||
case SET_EVERYONE_SUPPORT_E2EE:
|
||||
return {
|
||||
...state,
|
||||
everyoneSupportE2EE: action.everyoneSupportE2EE
|
||||
};
|
||||
|
||||
case SET_MAX_MODE: {
|
||||
return {
|
||||
|
|
|
@ -1,23 +1,74 @@
|
|||
// @flow
|
||||
|
||||
import StarIcon from '@atlaskit/icon/glyph/star';
|
||||
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
|
||||
import { Theme } from '@mui/material';
|
||||
import { ClassNameMap, withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createFeedbackOpenEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { isMobileBrowser } from '../../base/environment/utils';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import Input from '../../base/ui/components/web/Input';
|
||||
import { cancelFeedback, submitFeedback } from '../actions';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
dialog: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
rating: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: theme.spacing(4),
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
ratingLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(2),
|
||||
height: '20px'
|
||||
},
|
||||
|
||||
stars: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
starBtn: {
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'&:last-of-type': {
|
||||
marginRight: 0
|
||||
},
|
||||
|
||||
'&.active svg': {
|
||||
fill: theme.palette.success01
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
outline: `1px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
|
||||
details: {
|
||||
'& textarea': {
|
||||
minHeight: '122px'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const scoreAnimationClass
|
||||
= interfaceConfig.ENABLE_FEEDBACK_ANIMATION ? 'shake-rotate' : '';
|
||||
|
@ -34,49 +85,51 @@ const SCORES = [
|
|||
'feedback.veryGood'
|
||||
];
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
type Scrollable = {
|
||||
scroll: Function
|
||||
}
|
||||
scroll: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FeedbackDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The cached feedback message, if any, that was set when closing a previous
|
||||
* instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_message: string,
|
||||
_message: string;
|
||||
|
||||
/**
|
||||
* The cached feedback score, if any, that was set when closing a previous
|
||||
* instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_score: number,
|
||||
_score: number;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: ClassNameMap<string>;
|
||||
|
||||
/**
|
||||
* The JitsiConference that is being rated. The conference is passed in
|
||||
* because feedback can occur after a conference has been left, so
|
||||
* references to it may no longer exist in redux.
|
||||
*/
|
||||
conference: Object,
|
||||
conference: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Invoked to signal feedback submission or canceling.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code FeedbackDialog} is unmounted.
|
||||
*/
|
||||
onClose: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
onClose: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link FeedbackDialog}.
|
||||
|
@ -86,20 +139,20 @@ type State = {
|
|||
/**
|
||||
* The currently entered feedback message.
|
||||
*/
|
||||
message: string,
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The score selection index which is currently being hovered. The value -1
|
||||
* is used as a sentinel value to match store behavior of using -1 for no
|
||||
* score having been selected.
|
||||
*/
|
||||
mousedOverScore: number,
|
||||
mousedOverScore: number;
|
||||
|
||||
/**
|
||||
* The currently selected score selection index. The score will not be 0
|
||||
* indexed so subtract one to map with SCORES.
|
||||
*/
|
||||
score: number
|
||||
score: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -109,13 +162,19 @@ type State = {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class FeedbackDialog extends Component<Props, State> {
|
||||
class FeedbackDialog extends Component<IProps, State> {
|
||||
/**
|
||||
* An array of objects with click handlers for each of the scores listed in
|
||||
* the constant SCORES. This pattern is used for binding event handlers only
|
||||
* once for each score selection icon.
|
||||
*/
|
||||
_scoreClickConfigurations: Array<Object>;
|
||||
_scoreClickConfigurations: Array<{
|
||||
_onClick: (e: React.MouseEvent) => void;
|
||||
_onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
_onMouseOver: (e: React.MouseEvent) => void;
|
||||
}>;
|
||||
|
||||
_onScrollTop: (node: Scrollable | null) => void;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code FeedbackDialog} instance.
|
||||
|
@ -123,7 +182,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const { _message, _score } = this.props;
|
||||
|
@ -157,8 +216,9 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
|
||||
return {
|
||||
_onClick: () => this._onScoreSelect(index),
|
||||
_onKeyPres: e => {
|
||||
_onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._onScoreSelect(index);
|
||||
}
|
||||
|
@ -176,8 +236,8 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
|
||||
// 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);
|
||||
this._onScrollTop = (node: Scrollable | null) => {
|
||||
node?.scroll?.(0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -215,14 +275,14 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
const scoreToDisplayAsSelected
|
||||
= mousedOverScore > -1 ? mousedOverScore : score;
|
||||
|
||||
const { t } = this.props;
|
||||
const { classes, t } = this.props;
|
||||
|
||||
const scoreIcons = this._scoreClickConfigurations.map(
|
||||
(config, index) => {
|
||||
const isFilled = index <= scoreToDisplayAsSelected;
|
||||
const activeClass = isFilled ? 'active' : '';
|
||||
const className
|
||||
= `star-btn ${scoreAnimationClass} ${activeClass}`;
|
||||
= `${classes.starBtn} ${scoreAnimationClass} ${activeClass}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
|
@ -230,19 +290,19 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
className = { className }
|
||||
key = { index }
|
||||
onClick = { config._onClick }
|
||||
onKeyPress = { config._onKeyPres }
|
||||
onKeyDown = { config._onKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }
|
||||
{ ...(isMobileBrowser() ? {} : {
|
||||
onMouseOver: config._onMouseOver
|
||||
}) }>
|
||||
{ isFilled
|
||||
? <StarFilledIcon
|
||||
label = 'star-filled'
|
||||
size = 'xlarge' />
|
||||
: <StarIcon
|
||||
label = 'star'
|
||||
size = 'xlarge' /> }
|
||||
? <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavoriteSolid } />
|
||||
: <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavorite } /> }
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
@ -255,23 +315,24 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
}}
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
size = 'large'
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = 'feedback-dialog'>
|
||||
<div className = 'rating'>
|
||||
<div className = { classes.dialog }>
|
||||
<div className = { classes.rating }>
|
||||
<div
|
||||
aria-label = { this.props.t('feedback.star') }
|
||||
className = 'star-label' >
|
||||
className = { classes.ratingLabel } >
|
||||
<p id = 'starLabel'>
|
||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className = 'stars'
|
||||
className = { classes.stars }
|
||||
onMouseLeave = { this._onScoreContainerMouseLeave }>
|
||||
{ scoreIcons }
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'details'>
|
||||
<div className = { classes.details }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'feedbackTextArea'
|
||||
|
@ -285,8 +346,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
* Dispatches an action notifying feedback was not submitted. The submitted
|
||||
* score will have one added as the rest of the app does not expect 0
|
||||
|
@ -304,8 +363,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
return true;
|
||||
}
|
||||
|
||||
_onMessageChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the known entered feedback message.
|
||||
*
|
||||
|
@ -314,7 +371,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessageChange(newValue) {
|
||||
_onMessageChange(newValue: string) {
|
||||
this.setState({ message: newValue });
|
||||
}
|
||||
|
||||
|
@ -325,12 +382,10 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreSelect(score) {
|
||||
_onScoreSelect(score: number) {
|
||||
this.setState({ score });
|
||||
}
|
||||
|
||||
_onScoreContainerMouseLeave: () => void;
|
||||
|
||||
/**
|
||||
* Sets the currently hovered score to null to indicate no hover is
|
||||
* occurring.
|
||||
|
@ -350,12 +405,10 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreMouseOver(mousedOverScore) {
|
||||
_onScoreMouseOver(mousedOverScore: number) {
|
||||
this.setState({ mousedOverScore });
|
||||
}
|
||||
|
||||
_onSubmit: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches the entered feedback for submission. The submitted score will
|
||||
* have one added as the rest of the app does not expect 0 indexing.
|
||||
|
@ -373,8 +426,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onScrollTop: (node: ?Scrollable) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -386,7 +437,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @returns {{
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { message, score } = state['features/feedback'];
|
||||
|
||||
return {
|
||||
|
@ -407,4 +458,4 @@ function _mapStateToProps(state) {
|
|||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(FeedbackDialog));
|
||||
export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog)));
|
|
@ -13,7 +13,6 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
|||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { connect } from '../../../base/redux/functions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
|
@ -113,7 +112,7 @@ interface IProps extends WithTranslation {
|
|||
/**
|
||||
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
|
||||
*/
|
||||
_localScreenShare: IParticipant;
|
||||
_localScreenShareId: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip videos should currently be displayed.
|
||||
|
@ -333,7 +332,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
const {
|
||||
_currentLayout,
|
||||
_disableSelfView,
|
||||
_localScreenShare,
|
||||
_localScreenShareId,
|
||||
_mainFilmstripVisible,
|
||||
_resizableFilmstrip,
|
||||
_topPanelFilmstrip,
|
||||
|
@ -408,7 +407,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
}
|
||||
</div>
|
||||
)}
|
||||
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
|
||||
{_localScreenShareId && !_disableSelfView && !_verticalViewGrid && (
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalScreenShare'>
|
||||
|
@ -416,7 +415,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
{
|
||||
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && <Thumbnail
|
||||
key = 'localScreenShare'
|
||||
participantID = { _localScreenShare.id } />
|
||||
participantID = { _localScreenShareId } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -919,7 +918,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
|||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_isToolboxVisible: isToolboxVisible(state),
|
||||
_isVerticalFilmstrip,
|
||||
_localScreenShare: localScreenShare,
|
||||
_localScreenShareId: localScreenShare?.id,
|
||||
_mainFilmstripVisible: visible,
|
||||
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
|
||||
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
|
||||
|
|
|
@ -52,7 +52,6 @@ export function loadGoogleAPI() {
|
|||
return Promise.resolve();
|
||||
})
|
||||
.then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED)))
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => googleApi.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (isSignedIn) {
|
||||
|
@ -151,7 +150,8 @@ export function setGoogleAPIState(
|
|||
* selectedBoundStreamID: *} | never>)}
|
||||
*/
|
||||
export function showAccountSelection() {
|
||||
return () => googleApi.showAccountSelection(true);
|
||||
return () =>
|
||||
googleApi.showAccountSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,7 +161,7 @@ export function showAccountSelection() {
|
|||
*/
|
||||
export function signIn() {
|
||||
return (dispatch: Dispatch<any>) => googleApi.get()
|
||||
.then(() => googleApi.signInIfNotSignedIn(true))
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => dispatch({
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
|
||||
|
@ -205,10 +205,10 @@ export function updateProfile() {
|
|||
.then(profile => {
|
||||
dispatch({
|
||||
type: SET_GOOGLE_API_PROFILE,
|
||||
profileEmail: profile.email
|
||||
profileEmail: profile.getEmail()
|
||||
});
|
||||
|
||||
return profile.email;
|
||||
return profile.getEmail();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,10 @@ export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v
|
|||
/**
|
||||
* Array of API discovery doc URLs for APIs used by the googleApi.
|
||||
*
|
||||
* @type {string}
|
||||
* @type {string[]}
|
||||
*/
|
||||
export const DISCOVERY_DOCS = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';
|
||||
export const DISCOVERY_DOCS
|
||||
= [ 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest' ];
|
||||
|
||||
/**
|
||||
* An enumeration of the different states the Google API can be in.
|
||||
|
@ -60,13 +61,6 @@ export const GOOGLE_API_STATES = {
|
|||
*/
|
||||
export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar';
|
||||
|
||||
/**
|
||||
* Google API auth scope to access user email.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const GOOGLE_SCOPE_USERINFO = 'https://www.googleapis.com/auth/userinfo.email';
|
||||
|
||||
/**
|
||||
* Google API auth scope to access YouTube streams.
|
||||
*
|
||||
|
|
|
@ -3,13 +3,10 @@ import {
|
|||
API_URL_LIVE_BROADCASTS,
|
||||
DISCOVERY_DOCS,
|
||||
GOOGLE_SCOPE_CALENDAR,
|
||||
GOOGLE_SCOPE_USERINFO,
|
||||
GOOGLE_SCOPE_YOUTUBE
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
|
||||
const GOOGLE_GIS_LIBRARY_URL = 'https://accounts.google.com/gsi/client';
|
||||
|
||||
/**
|
||||
* A promise for dynamically loading the Google API Client Library.
|
||||
|
@ -53,9 +50,9 @@ const googleApi = {
|
|||
}
|
||||
|
||||
return this._getGoogleApiClient()
|
||||
.client.oauth2
|
||||
.userinfo.get().getPromise()
|
||||
.then(r => r.result);
|
||||
.auth2.getAuthInstance()
|
||||
.currentUser.get()
|
||||
.getBasicProfile();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -71,40 +68,23 @@ const googleApi = {
|
|||
initializeClient(clientId, enableYoutube, enableCalendar) {
|
||||
return this.get()
|
||||
.then(api => new Promise((resolve, reject) => {
|
||||
const scope
|
||||
= `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}`
|
||||
.trim();
|
||||
|
||||
// setTimeout is used as a workaround for api.client.init not
|
||||
// resolving consistently when the Google API Client Library is
|
||||
// loaded asynchronously. See:
|
||||
// github.com/google/google-api-javascript-client/issues/399
|
||||
setTimeout(() => {
|
||||
api.client.init({})
|
||||
.then(() => {
|
||||
if (enableCalendar) {
|
||||
api.client.load(DISCOVERY_DOCS);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
api.client.load('https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest');
|
||||
api.client.init({
|
||||
clientId,
|
||||
discoveryDocs: DISCOVERY_DOCS,
|
||||
scope
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, 500);
|
||||
}))
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scope
|
||||
= `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}`
|
||||
.trim();
|
||||
|
||||
this.tokenClient = this._getGoogleGISApiClient().accounts.oauth2.initTokenClient({
|
||||
// eslint-disable-next-line camelcase
|
||||
client_id: clientId,
|
||||
scope: `${scope} ${GOOGLE_SCOPE_USERINFO}`,
|
||||
callback: '' // defined at request time in await/promise scope.
|
||||
});
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
|
@ -115,38 +95,13 @@ const googleApi = {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
isSignedIn() {
|
||||
return new Promise((resolve, _) => {
|
||||
const te = parseInt(this.tokenExpires, 10);
|
||||
const isExpired = isNaN(this.tokenExpires) ? true : new Date().getTime() > te;
|
||||
|
||||
resolve(Boolean(!isExpired));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a script tag.
|
||||
*
|
||||
* @param {string} src - The source for the script tag.
|
||||
* @returns {Promise<unknown>}
|
||||
* @private
|
||||
*/
|
||||
_loadScriptTag(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
||||
scriptTag.async = true;
|
||||
scriptTag.addEventListener('error', () => {
|
||||
scriptTag.remove();
|
||||
|
||||
reject();
|
||||
});
|
||||
scriptTag.addEventListener('load', resolve);
|
||||
scriptTag.type = 'text/javascript';
|
||||
|
||||
scriptTag.src = src;
|
||||
|
||||
document.head.appendChild(scriptTag);
|
||||
});
|
||||
return this.get()
|
||||
.then(api => Boolean(api
|
||||
&& api.auth2
|
||||
&& api.auth2.getAuthInstance
|
||||
&& api.auth2.getAuthInstance()
|
||||
&& api.auth2.getAuthInstance().isSignedIn
|
||||
&& api.auth2.getAuthInstance().isSignedIn.get()));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -159,19 +114,29 @@ const googleApi = {
|
|||
return googleClientLoadPromise;
|
||||
}
|
||||
|
||||
googleClientLoadPromise = this._loadScriptTag(GOOGLE_API_CLIENT_LIBRARY_URL)
|
||||
.catch(() => {
|
||||
googleClientLoadPromise = new Promise((resolve, reject) => {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
||||
scriptTag.async = true;
|
||||
scriptTag.addEventListener('error', () => {
|
||||
scriptTag.remove();
|
||||
|
||||
googleClientLoadPromise = null;
|
||||
})
|
||||
|
||||
reject();
|
||||
});
|
||||
scriptTag.addEventListener('load', resolve);
|
||||
scriptTag.type = 'text/javascript';
|
||||
|
||||
scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL;
|
||||
|
||||
document.head.appendChild(scriptTag);
|
||||
})
|
||||
.then(() => new Promise((resolve, reject) =>
|
||||
this._getGoogleApiClient().load('client', {
|
||||
this._getGoogleApiClient().load('client:auth2', {
|
||||
callback: resolve,
|
||||
onerror: reject
|
||||
})))
|
||||
.then(this._loadScriptTag(GOOGLE_GIS_LIBRARY_URL))
|
||||
.catch(() => {
|
||||
googleClientLoadPromise = null;
|
||||
})
|
||||
.then(() => this._getGoogleApiClient());
|
||||
|
||||
return googleClientLoadPromise;
|
||||
|
@ -206,50 +171,25 @@ const googleApi = {
|
|||
* Prompts the participant to sign in to the Google API Client Library, even
|
||||
* if already signed in.
|
||||
*
|
||||
* @param {boolean} consent - Whether to show account selection dialog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
showAccountSelection(consent: boolean) {
|
||||
showAccountSelection() {
|
||||
return this.get()
|
||||
.then(api => new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Settle this promise in the response callback for requestAccessToken()
|
||||
this.tokenClient.callback = resp => {
|
||||
if (resp.error !== undefined) {
|
||||
reject(resp);
|
||||
}
|
||||
|
||||
// Get the number of seconds the token is valid for, subtract 5 minutes
|
||||
// to account for differences in clock settings and convert to ms.
|
||||
const expiresIn = (parseInt(api.client.getToken().expires_in, 10) - 300) * 1000;
|
||||
const now = new Date();
|
||||
const expireDate = new Date(now.getTime() + expiresIn);
|
||||
|
||||
this.tokenExpires = expireDate.getTime().toString();
|
||||
|
||||
resolve(resp);
|
||||
};
|
||||
|
||||
this.tokenClient.requestAccessToken({ prompt: consent ? 'consent' : '' });
|
||||
} catch (err) {
|
||||
logger.error('Error requesting token', err);
|
||||
}
|
||||
}));
|
||||
.then(api => api.auth2.getAuthInstance().signIn());
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library, if
|
||||
* not already signed in.
|
||||
*
|
||||
* @param {boolean} consent - Whether to show account selection dialog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
signInIfNotSignedIn(consent: boolean) {
|
||||
signInIfNotSignedIn() {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return this.showAccountSelection(consent);
|
||||
return this.showAccountSelection();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -261,10 +201,11 @@ const googleApi = {
|
|||
*/
|
||||
signOut() {
|
||||
return this.get()
|
||||
.then(() => {
|
||||
this.tokenClient = undefined;
|
||||
this.tokenExpires = undefined;
|
||||
});
|
||||
.then(api =>
|
||||
api.auth2
|
||||
&& api.auth2.getAuthInstance
|
||||
&& api.auth2.getAuthInstance()
|
||||
&& api.auth2.getAuthInstance().signOut());
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -446,17 +387,6 @@ const googleApi = {
|
|||
*/
|
||||
_getGoogleApiClient() {
|
||||
return window.gapi;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns the global Google Identity Services Library object.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
_getGoogleGISApiClient() {
|
||||
return window.google;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/base/redux');
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* The type of the action which signals the keyboard shortcuts dialog should
|
||||
* be displayed.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
* }
|
||||
*/
|
||||
export const OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
= 'OPEN_KEYBOARD_SHORTCUTS_DIALOG';
|
|
@ -1,14 +0,0 @@
|
|||
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Opens the dialog showing available keyboard shortcuts.
|
||||
*
|
||||
* @returns {{
|
||||
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
* }}
|
||||
*/
|
||||
export function openKeyboardShortcutsDialog() {
|
||||
return {
|
||||
type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
};
|
||||
}
|
|
@ -5,7 +5,8 @@ import { translate } from '../../../base/i18n';
|
|||
import { IconShortcuts } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
import { openKeyboardShortcutsDialog } from '../../actions';
|
||||
import { openSettingsDialog } from '../../../settings/actions';
|
||||
import { SETTINGS_TABS } from '../../../settings/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link KeyboardShortcutsButton}.
|
||||
|
@ -37,7 +38,7 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
|
|||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('shortcuts'));
|
||||
dispatch(openKeyboardShortcutsDialog());
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link KeyboardShortcutsDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* A Map with keyboard keys as keys and translation keys as values.
|
||||
*/
|
||||
shortcutDescriptions: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
list: {
|
||||
listStyleType: 'none',
|
||||
padding: 0,
|
||||
|
||||
'& .shortcuts-list__item': {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01
|
||||
},
|
||||
|
||||
'& .item-action': {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
...withPixelLineHeight(theme.typography.labelBold),
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const KeyboardShortcutsDialog = ({ shortcutDescriptions }: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const _renderShortcutsListItem = (keyboardKey: string, translationKey: string) => {
|
||||
let modifierKey = 'Alt';
|
||||
|
||||
if (window.navigator?.platform) {
|
||||
if (window.navigator.platform.indexOf('Mac') !== -1) {
|
||||
modifierKey = '⌥';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className = 'shortcuts-list__item'
|
||||
key = { keyboardKey }>
|
||||
<span
|
||||
aria-label = { t(translationKey) }
|
||||
className = 'shortcuts-list__description'>
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
<span className = 'item-action'>
|
||||
{keyboardKey.startsWith(':')
|
||||
? `${modifierKey} + ${keyboardKey.slice(1)}`
|
||||
: keyboardKey}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'keyboardShortcuts.keyboardShortcuts'>
|
||||
<div
|
||||
id = 'keyboard-shortcuts'>
|
||||
<ul
|
||||
className = { cx('shortcuts-list', classes.list) }
|
||||
id = 'keyboard-shortcuts-list'>
|
||||
{Array.from(shortcutDescriptions)
|
||||
.map(description => _renderShortcutsListItem(...description))}
|
||||
</ul>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyboardShortcutsDialog;
|
|
@ -1,2 +1 @@
|
|||
export { default as KeyboardShortcutsButton } from './KeyboardShortcutsButton';
|
||||
export { default as KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature keyboard-shortcuts.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(_store => next => action => {
|
||||
switch (action.type) {
|
||||
case OPEN_KEYBOARD_SHORTCUTS_DIALOG:
|
||||
if (typeof APP === 'object') {
|
||||
APP.keyboardshortcut.openDialog();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
|
@ -163,9 +162,9 @@ type Props = {
|
|||
participantID: ?string,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
* Callback used to stop a participant's video.
|
||||
*/
|
||||
stopVideo: Function,
|
||||
|
||||
/**
|
||||
* The translated "you" text.
|
||||
|
@ -192,17 +191,15 @@ function MeetingParticipantItem({
|
|||
_quickActionButtonType,
|
||||
_raisedHand,
|
||||
_videoMediaState,
|
||||
askUnmuteText,
|
||||
isHighlighted,
|
||||
isInBreakoutRoom,
|
||||
muteAudio,
|
||||
muteParticipantButtonText,
|
||||
onContextMenu,
|
||||
onLeave,
|
||||
openDrawerForParticipant,
|
||||
overflowDrawer,
|
||||
participantActionEllipsisLabel,
|
||||
t,
|
||||
stopVideo,
|
||||
youText
|
||||
}: Props) {
|
||||
|
||||
|
@ -242,12 +239,6 @@ function MeetingParticipantItem({
|
|||
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
||||
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
||||
|
||||
let askToUnmuteText = askUnmuteText;
|
||||
|
||||
if (_audioMediaState !== MEDIA_STATE.FORCE_MUTED && _videoMediaState === MEDIA_STATE.FORCE_MUTED) {
|
||||
askToUnmuteText = t('participantsPane.actions.allowVideo');
|
||||
}
|
||||
|
||||
return (
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
|
@ -273,16 +264,16 @@ function MeetingParticipantItem({
|
|||
&& <>
|
||||
{!isInBreakoutRoom && (
|
||||
<ParticipantQuickAction
|
||||
askUnmuteText = { askToUnmuteText }
|
||||
buttonType = { _quickActionButtonType }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
participantID = { _participantID }
|
||||
participantName = { _displayName } />
|
||||
participantName = { _displayName }
|
||||
stopVideo = { stopVideo } />
|
||||
)}
|
||||
<ParticipantActionEllipsis
|
||||
accessibilityLabel = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
onClick = { onContextMenu }
|
||||
participantID = { _participantID } />
|
||||
</>
|
||||
}
|
||||
|
||||
|
@ -318,7 +309,7 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
|
||||
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
|
||||
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state);
|
||||
|
||||
const tracks = state['features/base/tracks'];
|
||||
const _audioTrack = participantID === localParticipantId
|
||||
|
@ -342,4 +333,4 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MeetingParticipantItem));
|
||||
export default connect(_mapStateToProps)(MeetingParticipantItem);
|
||||
|
|
|
@ -66,6 +66,11 @@ type Props = {
|
|||
*/
|
||||
searchString?: string,
|
||||
|
||||
/**
|
||||
* Callback used to stop a participant's video.
|
||||
*/
|
||||
stopVideo: Function,
|
||||
|
||||
/**
|
||||
* The translated "you" text.
|
||||
*/
|
||||
|
@ -78,28 +83,25 @@ type Props = {
|
|||
* @returns {ReactNode}
|
||||
*/
|
||||
function MeetingParticipantItems({
|
||||
askUnmuteText,
|
||||
isInBreakoutRoom,
|
||||
lowerMenu,
|
||||
toggleMenu,
|
||||
muteAudio,
|
||||
muteParticipantButtonText,
|
||||
participantIds,
|
||||
openDrawerForParticipant,
|
||||
overflowDrawer,
|
||||
raiseContextId,
|
||||
participantActionEllipsisLabel,
|
||||
searchString,
|
||||
stopVideo,
|
||||
youText
|
||||
}: Props) {
|
||||
const renderParticipant = id => (
|
||||
<MeetingParticipantItem
|
||||
askUnmuteText = { askUnmuteText }
|
||||
isHighlighted = { raiseContextId === id }
|
||||
isInBreakoutRoom = { isInBreakoutRoom }
|
||||
key = { id }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
onContextMenu = { toggleMenu(id) }
|
||||
onLeave = { lowerMenu }
|
||||
openDrawerForParticipant = { openDrawerForParticipant }
|
||||
|
@ -107,6 +109,7 @@ function MeetingParticipantItems({
|
|||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||
participantID = { id }
|
||||
searchString = { searchString }
|
||||
stopVideo = { stopVideo }
|
||||
youText = { youText } />
|
||||
);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
||||
import { rejectParticipantAudio, rejectParticipantVideo } from '../../../av-moderation/actions';
|
||||
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
|
@ -88,11 +88,14 @@ function MeetingParticipants({
|
|||
const { t } = useTranslation();
|
||||
|
||||
const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
|
||||
|
||||
const muteAudio = useCallback(id => () => {
|
||||
dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
|
||||
dispatch(rejectParticipantAudio(id));
|
||||
}, [ dispatch ]);
|
||||
const stopVideo = useCallback(id => () => {
|
||||
dispatch(muteRemote(id, MEDIA_TYPE.VIDEO));
|
||||
dispatch(rejectParticipantVideo(id));
|
||||
}, [ dispatch ]);
|
||||
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
|
||||
|
||||
// FIXME:
|
||||
|
@ -103,8 +106,6 @@ function MeetingParticipants({
|
|||
// mounted.
|
||||
const participantActionEllipsisLabel = t('participantsPane.actions.moreParticipantOptions');
|
||||
const youText = t('chat.you');
|
||||
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
||||
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
||||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count);
|
||||
|
||||
|
@ -136,11 +137,9 @@ function MeetingParticipants({
|
|||
value = { searchString } />
|
||||
<div>
|
||||
<MeetingParticipantItems
|
||||
askUnmuteText = { askUnmuteText }
|
||||
isInBreakoutRoom = { isBreakoutRoom }
|
||||
lowerMenu = { lowerMenu }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
openDrawerForParticipant = { openDrawerForParticipant }
|
||||
overflowDrawer = { overflowDrawer }
|
||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||
|
@ -148,6 +147,7 @@ function MeetingParticipants({
|
|||
participantsCount = { participantsCount }
|
||||
raiseContextId = { raiseContext.entity }
|
||||
searchString = { normalizeAccents(searchString) }
|
||||
stopVideo = { stopVideo }
|
||||
toggleMenu = { toggleMenu }
|
||||
youText = { youText } />
|
||||
</div>
|
||||
|
|
|
@ -14,14 +14,17 @@ interface IProps {
|
|||
* Click handler function.
|
||||
*/
|
||||
onClick: () => void;
|
||||
|
||||
participantID?: string;
|
||||
}
|
||||
|
||||
const ParticipantActionEllipsis = ({ accessibilityLabel, onClick }: IProps) => (
|
||||
const ParticipantActionEllipsis = ({ accessibilityLabel, onClick, participantID }: IProps) => (
|
||||
<Button
|
||||
accessibilityLabel = { accessibilityLabel }
|
||||
icon = { IconDotsHorizontal }
|
||||
onClick = { onClick }
|
||||
size = 'small' />
|
||||
size = 'small'
|
||||
testId = { participantID ? `participant-more-options-${participantID}` : undefined } />
|
||||
);
|
||||
|
||||
export default ParticipantActionEllipsis;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { approveParticipant } from '../../../av-moderation/actions';
|
||||
import { approveParticipantAudio, approveParticipantVideo } from '../../../av-moderation/actions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { QUICK_ACTION_BUTTON } from '../../constants';
|
||||
|
||||
|
@ -43,6 +43,12 @@ interface IProps {
|
|||
* The name of the participant.
|
||||
*/
|
||||
participantName: string;
|
||||
|
||||
/**
|
||||
* Callback used to stop a participant's video.
|
||||
*/
|
||||
stopVideo: Function;
|
||||
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
|
@ -54,19 +60,22 @@ const useStyles = makeStyles()(theme => {
|
|||
});
|
||||
|
||||
const ParticipantQuickAction = ({
|
||||
askUnmuteText,
|
||||
buttonType,
|
||||
muteAudio,
|
||||
muteParticipantButtonText,
|
||||
participantID,
|
||||
participantName
|
||||
participantName,
|
||||
stopVideo
|
||||
}: IProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const askToUnmute = useCallback(() => {
|
||||
dispatch(approveParticipant(participantID));
|
||||
dispatch(approveParticipantAudio(participantID));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
const allowVideo = useCallback(() => {
|
||||
dispatch(approveParticipantVideo(participantID));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
switch (buttonType) {
|
||||
|
@ -75,10 +84,10 @@ const ParticipantQuickAction = ({
|
|||
<Button
|
||||
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
|
||||
className = { styles.button }
|
||||
label = { muteParticipantButtonText }
|
||||
label = { t('participantsPane.actions.mute') }
|
||||
onClick = { muteAudio(participantID) }
|
||||
size = 'small'
|
||||
testId = { `mute-${participantID}` } />
|
||||
testId = { `mute-audio-${participantID}` } />
|
||||
);
|
||||
}
|
||||
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
||||
|
@ -86,10 +95,32 @@ const ParticipantQuickAction = ({
|
|||
<Button
|
||||
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
|
||||
className = { styles.button }
|
||||
label = { askUnmuteText }
|
||||
label = { t('participantsPane.actions.askUnmute') }
|
||||
onClick = { askToUnmute }
|
||||
size = 'small'
|
||||
testId = { `unmute-${participantID}` } />
|
||||
testId = { `unmute-audio-${participantID}` } />
|
||||
);
|
||||
}
|
||||
case QUICK_ACTION_BUTTON.ALLOW_VIDEO: {
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
|
||||
className = { styles.button }
|
||||
label = { t('participantsPane.actions.allowVideo') }
|
||||
onClick = { allowVideo }
|
||||
size = 'small'
|
||||
testId = { `unmute-video-${participantID}` } />
|
||||
);
|
||||
}
|
||||
case QUICK_ACTION_BUTTON.STOP_VIDEO: {
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
|
||||
className = { styles.button }
|
||||
label = { t('participantsPane.actions.stopVideo') }
|
||||
onClick = { stopVideo(participantID) }
|
||||
size = 'small'
|
||||
testId = { `mute-video-${participantID}` } />
|
||||
);
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -36,19 +36,23 @@ export const MEDIA_STATE: { [key: string]: MediaState; } = {
|
|||
NONE: 'None'
|
||||
};
|
||||
|
||||
export type QuickActionButtonType = 'Mute' | 'AskToUnmute' | 'None';
|
||||
export type QuickActionButtonType = 'Mute' | 'AskToUnmute' | 'AllowVideo' | 'StopVideo' | 'None';
|
||||
|
||||
/**
|
||||
* Enum of possible participant mute button states.
|
||||
*/
|
||||
export const QUICK_ACTION_BUTTON: {
|
||||
ALLOW_VIDEO: QuickActionButtonType;
|
||||
ASK_TO_UNMUTE: QuickActionButtonType;
|
||||
MUTE: QuickActionButtonType;
|
||||
NONE: QuickActionButtonType;
|
||||
STOP_VIDEO: QuickActionButtonType;
|
||||
} = {
|
||||
ALLOW_VIDEO: 'AllowVideo',
|
||||
MUTE: 'Mute',
|
||||
ASK_TO_UNMUTE: 'AskToUnmute',
|
||||
NONE: 'None'
|
||||
NONE: 'None',
|
||||
STOP_VIDEO: 'StopVideo'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -131,15 +131,28 @@ export const getParticipantsPaneOpen = (state: IReduxState) => Boolean(getState(
|
|||
*
|
||||
* @param {IParticipant} participant - The participant.
|
||||
* @param {boolean} isAudioMuted - If audio is muted for the participant.
|
||||
* @param {boolean} isVideoMuted - If audio is muted for the participant.
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {string} - The type of the quick action button.
|
||||
*/
|
||||
export function getQuickActionButtonType(participant: IParticipant, isAudioMuted: Boolean, state: IReduxState) {
|
||||
export function getQuickActionButtonType(
|
||||
participant: IParticipant,
|
||||
isAudioMuted: Boolean,
|
||||
isVideoMuted: Boolean,
|
||||
state: IReduxState) {
|
||||
// handled only by moderators
|
||||
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||
|
||||
if (isLocalParticipantModerator(state)) {
|
||||
if (!isAudioMuted) {
|
||||
return QUICK_ACTION_BUTTON.MUTE;
|
||||
}
|
||||
if (!isVideoMuted) {
|
||||
return QUICK_ACTION_BUTTON.STOP_VIDEO;
|
||||
}
|
||||
if (isVideoForceMuted) {
|
||||
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
|
||||
}
|
||||
if (isSupported()(state)) {
|
||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
|
|||
<div className = { classes.footer }>
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.create') }
|
||||
autoFocus = { true }
|
||||
fullWidth = { true }
|
||||
labelKey = { 'polls.create.create' }
|
||||
onClick = { onCreate } />
|
||||
|
|
|
@ -68,11 +68,15 @@ export function connectAndSendIdentity({ getState, dispatch }: IStore, identity:
|
|||
// This is done in order to facilitate queries based on different conference configurations.
|
||||
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
|
||||
// conference with a specific version.
|
||||
// XXX(george): we also want to be able to correlate between rtcstats and callstats, so we're
|
||||
// appending the callstats user name (if it exists) to the display name.
|
||||
const displayName = options.statisticsId
|
||||
|| options.statisticsDisplayName
|
||||
|| jitsiLocalStorage.getItem('callStatsUserName');
|
||||
let displayName = jitsiLocalStorage.getItem('callStatsUserName');
|
||||
|
||||
if (options.statisticsId || options.statisticsDisplayName) {
|
||||
if (options.statisticsId && options.statisticsDisplayName) {
|
||||
displayName = `${options.statisticsDisplayName} (${options.statisticsId})`;
|
||||
} else {
|
||||
displayName = options.statisticsId || options.statisticsDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
RTCStats.sendIdentityData({
|
||||
...getAmplitudeIdentity(),
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './actions.any';
|
|
@ -1,5 +1,7 @@
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
import { IStore } from '../app/types';
|
||||
import {
|
||||
setFollowMe,
|
||||
|
@ -9,7 +11,8 @@ import {
|
|||
import { openDialog } from '../base/dialog/actions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions';
|
||||
import { toggleBackgroundEffect } from '../virtual-background/actions';
|
||||
import virtualBackgroundLogger from '../virtual-background/logger';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
|
@ -21,8 +24,10 @@ import { LogoutDialog, SettingsDialog } from './components';
|
|||
import {
|
||||
getModeratorTabProps,
|
||||
getMoreTabProps,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getSoundsTabProps
|
||||
getShortcutsTabProps,
|
||||
getVirtualBackgroundTabProps
|
||||
} from './functions';
|
||||
|
||||
/**
|
||||
|
@ -96,31 +101,6 @@ export function submitMoreTab(newState: any) {
|
|||
}));
|
||||
}
|
||||
|
||||
const enabledNotifications = newState.enabledNotifications;
|
||||
|
||||
if (enabledNotifications !== currentState.enabledNotifications) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedNotifications: {
|
||||
...getState()['features/base/settings'].userSelectedNotifications,
|
||||
...enabledNotifications
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (newState.currentLanguage !== currentState.currentLanguage) {
|
||||
i18next.changeLanguage(newState.currentLanguage);
|
||||
}
|
||||
|
||||
if (newState.currentFramerate !== currentState.currentFramerate) {
|
||||
const frameRate = parseInt(newState.currentFramerate, 10);
|
||||
|
||||
dispatch(setScreenshareFramerate(frameRate));
|
||||
}
|
||||
|
||||
if (newState.hideSelfView !== currentState.hideSelfView) {
|
||||
dispatch(updateSettings({ disableSelfView: newState.hideSelfView }));
|
||||
}
|
||||
|
||||
if (newState.maxStageParticipants !== currentState.maxStageParticipants) {
|
||||
dispatch(updateSettings({ maxStageParticipants: Number(newState.maxStageParticipants) }));
|
||||
}
|
||||
|
@ -174,6 +154,14 @@ export function submitProfileTab(newState: any) {
|
|||
if (newState.email !== currentState.email) {
|
||||
APP.conference.changeLocalEmail(newState.email);
|
||||
}
|
||||
|
||||
if (newState.hideSelfView !== currentState.hideSelfView) {
|
||||
dispatch(updateSettings({ disableSelfView: newState.hideSelfView }));
|
||||
}
|
||||
|
||||
if (newState.currentLanguage !== currentState.currentLanguage) {
|
||||
i18next.changeLanguage(newState.currentLanguage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -183,9 +171,9 @@ export function submitProfileTab(newState: any) {
|
|||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitSoundsTab(newState: any) {
|
||||
export function submitNotificationsTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getSoundsTabProps(getState());
|
||||
const currentState = getNotificationsTabProps(getState());
|
||||
const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted;
|
||||
const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
|
||||
|| (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
|
||||
|
@ -209,6 +197,17 @@ export function submitSoundsTab(newState: any) {
|
|||
}
|
||||
dispatch(updateSettings(settingsToUpdate));
|
||||
}
|
||||
|
||||
const enabledNotifications = newState.enabledNotifications;
|
||||
|
||||
if (enabledNotifications !== currentState.enabledNotifications) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedNotifications: {
|
||||
...getState()['features/base/settings'].userSelectedNotifications,
|
||||
...enabledNotifications
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -237,3 +236,47 @@ export function toggleVideoSettings() {
|
|||
dispatch(setVideoSettingsVisibility(!value));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Shortcuts" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitShortcutsTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getShortcutsTabProps(getState());
|
||||
|
||||
if (newState.keyboardShortcutsEnabled !== currentState.keyboardShortcutsEnabled) {
|
||||
keyboardShortcut.enable(newState.keyboardShortcutsEnabled);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Virtual Background" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isCancel - Whether the change represents a cancel.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitVirtualBackgroundTab(newState: any, isCancel = false) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getVirtualBackgroundTabProps(getState());
|
||||
|
||||
if (newState.options?.selectedThumbnail) {
|
||||
await dispatch(toggleBackgroundEffect(newState.options, currentState._jitsiTrack));
|
||||
|
||||
if (!isCancel) {
|
||||
// Set x scale to default value.
|
||||
dispatch(updateSettings({
|
||||
localFlipX: true
|
||||
}));
|
||||
|
||||
virtualBackgroundLogger.info(`Virtual background type: '${
|
||||
typeof newState.options.backgroundType === 'undefined'
|
||||
? 'none' : newState.options.backgroundType}' applied!`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
// @ts-ignore
|
||||
import { AbstractDialogTab } from '../../../base/dialog';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ModeratorTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* If set hides the reactions moderation setting.
|
||||
|
@ -46,11 +51,25 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
* enabled.
|
||||
*/
|
||||
startVideoMuted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
|
||||
title: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
color: `${theme.palette.text01} !important`,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -58,14 +77,14 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ModeratorTab extends AbstractDialogTab<Props> {
|
||||
class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code ModeratorTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
|
@ -75,16 +94,6 @@ class ModeratorTab extends AbstractDialogTab<Props> {
|
|||
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return <div className = 'moderator-tab box'>{ this._renderModeratorSettings() }</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if conferences should start
|
||||
* with audio muted.
|
||||
|
@ -134,58 +143,59 @@ class ModeratorTab extends AbstractDialogTab<Props> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying conference-wide settings.
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @private
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderModeratorSettings() {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
disableReactionsModeration,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
startAudioMuted,
|
||||
startVideoMuted,
|
||||
startReactionsMuted,
|
||||
t // @ts-ignore
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
className = { `moderator-tab ${classes.container}` }
|
||||
key = 'moderator'>
|
||||
<div className = 'moderator-settings-wrapper'>
|
||||
<Checkbox
|
||||
checked = { startAudioMuted }
|
||||
className = 'settings-checkbox'
|
||||
label = { t('settings.startAudioMuted') }
|
||||
name = 'start-audio-muted'
|
||||
onChange = { this._onStartAudioMutedChanged } />
|
||||
<Checkbox
|
||||
checked = { startVideoMuted }
|
||||
className = 'settings-checkbox'
|
||||
label = { t('settings.startVideoMuted') }
|
||||
name = 'start-video-muted'
|
||||
onChange = { this._onStartVideoMutedChanged } />
|
||||
<Checkbox
|
||||
checked = { followMeEnabled && !followMeActive }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { followMeActive }
|
||||
label = { t('settings.followMe') }
|
||||
name = 'follow-me'
|
||||
onChange = { this._onFollowMeEnabledChanged } />
|
||||
{ !disableReactionsModeration
|
||||
<h2 className = { classes.title }>
|
||||
{t('settings.moderatorOptions')}
|
||||
</h2>
|
||||
<Checkbox
|
||||
checked = { startAudioMuted }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startAudioMuted') }
|
||||
name = 'start-audio-muted'
|
||||
onChange = { this._onStartAudioMutedChanged } />
|
||||
<Checkbox
|
||||
checked = { startVideoMuted }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startVideoMuted') }
|
||||
name = 'start-video-muted'
|
||||
onChange = { this._onStartVideoMutedChanged } />
|
||||
<Checkbox
|
||||
checked = { followMeEnabled && !followMeActive }
|
||||
className = { classes.checkbox }
|
||||
disabled = { followMeActive }
|
||||
label = { t('settings.followMe') }
|
||||
name = 'follow-me'
|
||||
onChange = { this._onFollowMeEnabledChanged } />
|
||||
{ !disableReactionsModeration
|
||||
&& <Checkbox
|
||||
checked = { startReactionsMuted }
|
||||
className = 'settings-checkbox'
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startReactionsMuted') }
|
||||
name = 'start-reactions-muted'
|
||||
onChange = { this._onStartReactionsMutedChanged } /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default translate(ModeratorTab);
|
||||
export default withStyles(styles)(translate(ModeratorTab));
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
// @ts-expect-error
|
||||
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps
|
||||
} from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
|
@ -10,74 +11,32 @@ import { translate } from '../../../base/i18n/functions';
|
|||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip/constants';
|
||||
import { SS_DEFAULT_FRAME_RATE } from '../../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link MoreTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* The currently selected desktop share frame rate in the frame rate select dropdown.
|
||||
* CSS classes object.
|
||||
*/
|
||||
currentFramerate: string;
|
||||
|
||||
/**
|
||||
* The currently selected language to display in the language select
|
||||
* dropdown.
|
||||
*/
|
||||
currentLanguage: string;
|
||||
|
||||
/**
|
||||
* All available desktop capture frame rates.
|
||||
*/
|
||||
desktopShareFramerates: Array<number>;
|
||||
|
||||
/**
|
||||
* Whether to show hide self view setting.
|
||||
*/
|
||||
disableHideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* The types of enabled notifications that can be configured and their specific visibility.
|
||||
*/
|
||||
enabledNotifications: Object;
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not follow me is currently active (enabled by some other participant).
|
||||
*/
|
||||
followMeActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to hide self-view screen.
|
||||
*/
|
||||
hideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* All available languages to display in the language select dropdown.
|
||||
*/
|
||||
languages: Array<string>;
|
||||
|
||||
/**
|
||||
* The number of max participants to display on stage.
|
||||
*/
|
||||
maxStageParticipants: number;
|
||||
|
||||
/**
|
||||
* Whether or not to display the language select dropdown.
|
||||
*/
|
||||
showLanguageSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display moderator-only settings.
|
||||
*/
|
||||
showModeratorSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display notifications settings.
|
||||
*/
|
||||
showNotificationsSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to show prejoin screen.
|
||||
*/
|
||||
|
@ -92,11 +51,23 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
* Wether or not the stage filmstrip is enabled.
|
||||
*/
|
||||
stageFilmstripEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
|
||||
divider: {
|
||||
margin: `${theme.spacing(4)} 0`,
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
border: 0,
|
||||
backgroundColor: theme.palette.ui03
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -104,23 +75,18 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MoreTab extends AbstractDialogTab<Props, any> {
|
||||
class MoreTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code MoreTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
|
||||
this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
|
||||
this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this);
|
||||
this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
|
||||
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
|
||||
this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this);
|
||||
this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
|
||||
this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
|
||||
}
|
||||
|
@ -132,46 +98,21 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const content = [];
|
||||
|
||||
content.push(this._renderSettingsLeft());
|
||||
content.push(this._renderSettingsRight());
|
||||
const { showPrejoinSettings, classes } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'more-tab box'
|
||||
className = { clsx('more-tab', classes.container) }
|
||||
key = 'more'>
|
||||
{ content }
|
||||
{showPrejoinSettings && <>
|
||||
{this._renderPrejoinScreenSettings()}
|
||||
<hr className = { classes.divider } />
|
||||
</>}
|
||||
{this._renderMaxStageParticipantsSelect()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a frame rate from the select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const frameRate = e.target.value;
|
||||
|
||||
super._onChange({ currentFramerate: frameRate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a language from select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLanguageItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const language = e.target.value;
|
||||
|
||||
super._onChange({ currentLanguage: language });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if the lobby
|
||||
* should be shown.
|
||||
|
@ -184,48 +125,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
super._onChange({ showPrejoinPage: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if the given type of
|
||||
* notifications should be shown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @param {string} type - The type of the notification.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onEnabledNotificationsChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>, type: any) {
|
||||
super._onChange({
|
||||
enabledNotifications: {
|
||||
...this.props.enabledNotifications,
|
||||
[type]: checked
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if global keyboard shortcuts
|
||||
* should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyboardShortcutEnableChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
keyboardShortcut.enable(checked);
|
||||
super._onChange({ keyboardShortcutEnable: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if hide self view should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHideSelfViewChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ hideSelfView: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a max number of stage participants from the select dropdown.
|
||||
*
|
||||
|
@ -239,124 +138,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
super._onChange({ maxStageParticipants: maxParticipants });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for the desktop share frame rate dropdown.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFramerateSelect() {
|
||||
const { currentFramerate, desktopShareFramerates, t } = this.props;
|
||||
const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
|
||||
return {
|
||||
value: frameRate,
|
||||
label: `${frameRate} ${t('settings.framesPerSecond')}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'frameRate'>
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
|
||||
? t('settings.desktopShareHighFpsWarning')
|
||||
: t('settings.desktopShareWarning') }
|
||||
label = { t('settings.desktopShareFramerate') }
|
||||
onChange = { this._onFramerateItemSelect }
|
||||
options = { frameRateItems }
|
||||
value = { currentFramerate } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for keyboardShortcut settings.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderKeyboardShortcutCheckbox() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'keyboard-shortcut'>
|
||||
<span className = 'checkbox-label'>
|
||||
{ t('keyboardShortcuts.keyboardShortcuts') }
|
||||
</span>
|
||||
<Checkbox
|
||||
checked = { keyboardShortcut.getEnabled() }
|
||||
label = { t('prejoin.keyboardShortcuts') }
|
||||
name = 'enable-keyboard-shortcuts'
|
||||
onChange = { this._onKeyboardShortcutEnableChanged } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for self view setting.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSelfViewCheckbox() {
|
||||
const { hideSelfView, t } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'selfview'>
|
||||
<span className = 'checkbox-label'>
|
||||
{ t('settings.selfView') }
|
||||
</span>
|
||||
<Checkbox
|
||||
checked = { hideSelfView }
|
||||
label = { t('videothumbnail.hideSelfView') }
|
||||
name = 'hide-self-view'
|
||||
onChange = { this._onHideSelfViewChanged } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu item for changing displayed language.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLanguageSelect() {
|
||||
const {
|
||||
currentLanguage,
|
||||
languages,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const languageItems
|
||||
= languages.map((language: string) => {
|
||||
return {
|
||||
value: language,
|
||||
label: t(`languages:${language}`)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'language'>
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
label = { t('settings.language') }
|
||||
onChange = { this._onLanguageItemSelect }
|
||||
options = { languageItems }
|
||||
value = { currentLanguage } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying prejoin screen settings.
|
||||
*
|
||||
|
@ -367,49 +148,11 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
const { t, showPrejoinPage } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'prejoin-screen'>
|
||||
<span className = 'checkbox-label'>
|
||||
{ t('prejoin.premeeting') }
|
||||
</span>
|
||||
<Checkbox
|
||||
checked = { showPrejoinPage }
|
||||
label = { t('prejoin.showScreen') }
|
||||
name = 'show-prejoin-page'
|
||||
onChange = { this._onShowPrejoinPageChanged } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying the enabled notifications settings.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNotificationsSettings() {
|
||||
const { t, enabledNotifications } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'notifications'>
|
||||
<span className = 'checkbox-label'>
|
||||
{ t('notify.displayNotifications') }
|
||||
</span>
|
||||
{
|
||||
Object.keys(enabledNotifications).map(key => (
|
||||
<Checkbox
|
||||
checked = { Boolean(enabledNotifications[key as keyof typeof enabledNotifications]) }
|
||||
key = { key }
|
||||
label = { t(key) }
|
||||
name = { `show-${key}` }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onChange = { e => this._onEnabledNotificationsChanged(e, key) } />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked = { showPrejoinPage }
|
||||
label = { t('prejoin.showScreen') }
|
||||
name = 'show-prejoin-page'
|
||||
onChange = { this._onShowPrejoinPageChanged } />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -433,59 +176,13 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'maxStageParticipants'>
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
value = { maxStageParticipants } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React element that needs to be displayed on the right half of the more tabs.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSettingsRight() {
|
||||
const { showLanguageSettings } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane right'
|
||||
key = 'settings-sub-pane-right'>
|
||||
{ showLanguageSettings && this._renderLanguageSelect() }
|
||||
{ this._renderFramerateSelect() }
|
||||
{ this._renderMaxStageParticipantsSelect() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React element that needs to be displayed on the left half of the more tabs.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSettingsLeft() {
|
||||
const { disableHideSelfView, showNotificationsSettings, showPrejoinSettings } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane left'
|
||||
key = 'settings-sub-pane-left'>
|
||||
{ showPrejoinSettings && this._renderPrejoinScreenSettings() }
|
||||
{ showNotificationsSettings && this._renderNotificationsSettings() }
|
||||
{ this._renderKeyboardShortcutCheckbox() }
|
||||
{ !disableHideSelfView && this._renderSelfViewCheckbox() }
|
||||
</div>
|
||||
<Select
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
value = { maxStageParticipants } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(MoreTab);
|
||||
export default withStyles(styles)(translate(MoreTab));
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link NotificationsTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Array of disabled sounds ids.
|
||||
*/
|
||||
disabledSounds: string[];
|
||||
|
||||
/**
|
||||
* Whether or not the reactions feature is enabled.
|
||||
*/
|
||||
enableReactions: Boolean;
|
||||
|
||||
/**
|
||||
* The types of enabled notifications that can be configured and their specific visibility.
|
||||
*/
|
||||
enabledNotifications: Object;
|
||||
|
||||
/**
|
||||
* Whether or not moderator muted the sounds.
|
||||
*/
|
||||
moderatorMutedSoundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display notifications settings.
|
||||
*/
|
||||
showNotificationsSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether sound settings should be displayed or not.
|
||||
*/
|
||||
showSoundsSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the incoming message should play.
|
||||
*/
|
||||
soundsIncomingMessage: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant joined should play.
|
||||
*/
|
||||
soundsParticipantJoined: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant entering the lobby should play.
|
||||
*/
|
||||
soundsParticipantKnocking: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant left should play.
|
||||
*/
|
||||
soundsParticipantLeft: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for reactions should play.
|
||||
*/
|
||||
soundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the talk while muted notification should play.
|
||||
*/
|
||||
soundsTalkWhileMuted: Boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
column: {
|
||||
flex: 1,
|
||||
|
||||
'&:first-child:not(:last-child)': {
|
||||
marginRight: theme.spacing(3)
|
||||
}
|
||||
},
|
||||
|
||||
title: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
color: `${theme.palette.text01} !important`,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's sound settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class NotificationsTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code SoundsTab} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code SoundsTab} instance with.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a sound setting state.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChange({ target }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ [target.name]: target.checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if the given type of
|
||||
* notifications should be shown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @param {string} type - The type of the notification.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onEnabledNotificationsChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>, type: any) {
|
||||
super._onChange({
|
||||
enabledNotifications: {
|
||||
...this.props.enabledNotifications,
|
||||
[type]: checked
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
disabledSounds,
|
||||
enabledNotifications,
|
||||
showNotificationsSettings,
|
||||
showSoundsSettings,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions,
|
||||
enableReactions,
|
||||
moderatorMutedSoundsReactions,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
key = 'sounds'>
|
||||
{showSoundsSettings && (
|
||||
<div className = { classes.column }>
|
||||
<h2 className = { classes.title }>
|
||||
{t('settings.playSounds')}
|
||||
</h2>
|
||||
{enableReactions && <Checkbox
|
||||
checked = { soundsReactions && !disabledSounds.includes('REACTION_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { Boolean(moderatorMutedSoundsReactions
|
||||
|| disabledSounds.includes('REACTION_SOUND')) }
|
||||
label = { t('settings.reactions') }
|
||||
name = 'soundsReactions'
|
||||
onChange = { this._onChange } />
|
||||
}
|
||||
<Checkbox
|
||||
checked = { soundsIncomingMessage && !disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
label = { t('settings.incomingMessage') }
|
||||
name = 'soundsIncomingMessage'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantJoined
|
||||
&& !disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
label = { t('settings.participantJoined') }
|
||||
name = 'soundsParticipantJoined'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantLeft && !disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
label = { t('settings.participantLeft') }
|
||||
name = 'soundsParticipantLeft'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsTalkWhileMuted && !disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
label = { t('settings.talkWhileMuted') }
|
||||
name = 'soundsTalkWhileMuted'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantKnocking
|
||||
&& !disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
label = { t('settings.participantKnocking') }
|
||||
name = 'soundsParticipantKnocking'
|
||||
onChange = { this._onChange } />
|
||||
</div>
|
||||
)}
|
||||
{showNotificationsSettings && (
|
||||
<div className = { classes.column }>
|
||||
<h2 className = { classes.title }>
|
||||
{t('notify.displayNotifications')}
|
||||
</h2>
|
||||
{
|
||||
Object.keys(enabledNotifications).map(key => (
|
||||
<Checkbox
|
||||
checked = { Boolean(enabledNotifications[key as
|
||||
keyof typeof enabledNotifications]) }
|
||||
className = { classes.checkbox }
|
||||
key = { key }
|
||||
label = { t(key) }
|
||||
name = { `show-${key}` }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onChange = { e => this._onEnabledNotificationsChanged(e, key) } />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(translate(NotificationsTab));
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -6,20 +7,22 @@ import { WithTranslation } from 'react-i18next';
|
|||
import UIEvents from '../../../../../service/UI/UIEvents';
|
||||
import { createProfilePanelButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { AbstractDialogTab } from '../../../base/dialog';
|
||||
// @ts-ignore
|
||||
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import { openLogoutDialog } from '../../actions';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ProfileTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not server-side authentication is available.
|
||||
|
@ -31,6 +34,22 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*/
|
||||
authLogin: string;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* The currently selected language to display in the language select
|
||||
* dropdown.
|
||||
*/
|
||||
currentLanguage: string;
|
||||
|
||||
/**
|
||||
* Whether to show hide self view setting.
|
||||
*/
|
||||
disableHideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* The display name to display for the local participant.
|
||||
*/
|
||||
|
@ -46,15 +65,52 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*/
|
||||
hideEmailInSettings?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to hide self-view screen.
|
||||
*/
|
||||
hideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* The id of the local participant.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* All available languages to display in the language select dropdown.
|
||||
*/
|
||||
languages: Array<string>;
|
||||
|
||||
/**
|
||||
* If the display name is read only.
|
||||
*/
|
||||
readOnlyName: boolean;
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
* Whether or not to display the language select dropdown.
|
||||
*/
|
||||
t: Function;
|
||||
showLanguageSettings: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
width: '100%',
|
||||
padding: '0 2px'
|
||||
},
|
||||
|
||||
avatarContainer: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
bottomMargin: {
|
||||
marginBottom: theme.spacing(4)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -62,7 +118,7 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ProfileTab extends AbstractDialogTab<Props> {
|
||||
class ProfileTab extends AbstractDialogTab<IProps, any> {
|
||||
static defaultProps = {
|
||||
displayName: '',
|
||||
email: ''
|
||||
|
@ -71,16 +127,18 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
/**
|
||||
* Initializes a new {@code ConnectedSettingsDialog} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code ConnectedSettingsDialog} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onAuthToggle = this._onAuthToggle.bind(this);
|
||||
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
|
||||
this._onEmailChange = this._onEmailChange.bind(this);
|
||||
this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this);
|
||||
this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,6 +163,62 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
super._onChange({ email: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if hide self view should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHideSelfViewChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ hideSelfView: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a language from select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLanguageItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const language = e.target.value;
|
||||
|
||||
super._onChange({ currentLanguage: language });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu item for changing displayed language.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLanguageSelect() {
|
||||
const {
|
||||
classes,
|
||||
currentLanguage,
|
||||
languages,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const languageItems
|
||||
= languages.map((language: string) => {
|
||||
return {
|
||||
value: language,
|
||||
label: t(`languages:${language}`)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
className = { classes.bottomMargin }
|
||||
label = { t('settings.language') }
|
||||
onChange = { this._onLanguageItemSelect }
|
||||
options = { languageItems }
|
||||
value = { currentLanguage } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -114,38 +228,55 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
render() {
|
||||
const {
|
||||
authEnabled,
|
||||
classes,
|
||||
disableHideSelfView,
|
||||
displayName,
|
||||
email,
|
||||
hideEmailInSettings,
|
||||
hideSelfView,
|
||||
id,
|
||||
readOnlyName,
|
||||
t // @ts-ignore
|
||||
showLanguageSettings,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className = 'profile-edit'>
|
||||
<div className = 'profile-edit-field'>
|
||||
<Input
|
||||
disabled = { readOnlyName }
|
||||
id = 'setDisplayName'
|
||||
label = { t('profile.setDisplayNameLabel') }
|
||||
name = 'name'
|
||||
onChange = { this._onDisplayNameChange }
|
||||
placeholder = { t('settings.name') }
|
||||
type = 'text'
|
||||
value = { displayName } />
|
||||
</div>
|
||||
{!hideEmailInSettings && <div className = 'profile-edit-field'>
|
||||
<Input
|
||||
id = 'setEmail'
|
||||
label = { t('profile.setEmailLabel') }
|
||||
name = 'email'
|
||||
onChange = { this._onEmailChange }
|
||||
placeholder = { t('profile.setEmailInput') }
|
||||
type = 'text'
|
||||
value = { email } />
|
||||
</div>}
|
||||
<div className = { classes.container } >
|
||||
<div className = { classes.avatarContainer }>
|
||||
<Avatar
|
||||
participantId = { id }
|
||||
size = { 60 } />
|
||||
</div>
|
||||
<Input
|
||||
className = { classes.bottomMargin }
|
||||
disabled = { readOnlyName }
|
||||
id = 'setDisplayName'
|
||||
label = { t('profile.setDisplayNameLabel') }
|
||||
name = 'name'
|
||||
onChange = { this._onDisplayNameChange }
|
||||
placeholder = { t('settings.name') }
|
||||
type = 'text'
|
||||
value = { displayName } />
|
||||
{!hideEmailInSettings && <div className = 'profile-edit-field'>
|
||||
<Input
|
||||
className = { classes.bottomMargin }
|
||||
id = 'setEmail'
|
||||
label = { t('profile.setEmailLabel') }
|
||||
name = 'email'
|
||||
onChange = { this._onEmailChange }
|
||||
placeholder = { t('profile.setEmailInput') }
|
||||
type = 'text'
|
||||
value = { email } />
|
||||
</div>}
|
||||
{!disableHideSelfView && (
|
||||
<Checkbox
|
||||
checked = { hideSelfView }
|
||||
className = { classes.bottomMargin }
|
||||
label = { t('videothumbnail.hideSelfView') }
|
||||
name = 'hide-self-view'
|
||||
onChange = { this._onHideSelfViewChanged } />
|
||||
)}
|
||||
{showLanguageSettings && this._renderLanguageSelect()}
|
||||
{ authEnabled && this._renderAuth() }
|
||||
</div>
|
||||
);
|
||||
|
@ -159,7 +290,6 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onAuthToggle() {
|
||||
// @ts-ignore
|
||||
if (this.props.authLogin) {
|
||||
sendAnalytics(createProfilePanelButtonEvent('logout.button'));
|
||||
|
||||
|
@ -183,8 +313,6 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
const {
|
||||
authLogin,
|
||||
t
|
||||
|
||||
// @ts-ignore
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -206,5 +334,4 @@ class ProfileTab extends AbstractDialogTab<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default translate(ProfileTab);
|
||||
export default withStyles(styles)(translate(ProfileTab));
|
||||
|
|
|
@ -46,7 +46,7 @@ class SettingsButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { defaultTab = SETTINGS_TABS.DEVICES, dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
const { defaultTab = SETTINGS_TABS.AUDIO, dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('settings'));
|
||||
dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));
|
||||
|
|
|
@ -1,41 +1,58 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconBell, IconCalendar, IconGear, IconModerator, IconUser, IconVolumeUp } from '../../../base/icons/svg';
|
||||
import {
|
||||
IconBell,
|
||||
IconCalendar,
|
||||
IconGear,
|
||||
IconHost,
|
||||
IconImage,
|
||||
IconShortcuts,
|
||||
IconUser,
|
||||
IconVideo,
|
||||
IconVolumeUp
|
||||
} from '../../../base/icons/svg';
|
||||
import { connect } from '../../../base/redux/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions.web';
|
||||
import { submitAudioDeviceSelectionTab, submitVideoDeviceSelectionTab } from '../../../device-selection/actions.web';
|
||||
import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection';
|
||||
import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection';
|
||||
import {
|
||||
DeviceSelection,
|
||||
getDeviceSelectionDialogProps,
|
||||
submitDeviceSelectionTab
|
||||
// @ts-ignore
|
||||
} from '../../../device-selection';
|
||||
getAudioDeviceSelectionDialogProps,
|
||||
getVideoDeviceSelectionDialogProps
|
||||
} from '../../../device-selection/functions.web';
|
||||
import { checkBlurSupport } from '../../../virtual-background/functions';
|
||||
import {
|
||||
submitModeratorTab,
|
||||
submitMoreTab,
|
||||
submitNotificationsTab,
|
||||
submitProfileTab,
|
||||
submitSoundsTab
|
||||
submitShortcutsTab,
|
||||
submitVirtualBackgroundTab
|
||||
} from '../../actions';
|
||||
import { SETTINGS_TABS } from '../../constants';
|
||||
import {
|
||||
getModeratorTabProps,
|
||||
getMoreTabProps,
|
||||
getNotificationsMap,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getSoundsTabProps
|
||||
getShortcutsTabProps,
|
||||
getVirtualBackgroundTabProps
|
||||
} from '../../functions';
|
||||
|
||||
// @ts-ignore
|
||||
import CalendarTab from './CalendarTab';
|
||||
import ModeratorTab from './ModeratorTab';
|
||||
import MoreTab from './MoreTab';
|
||||
import NotificationsTab from './NotificationsTab';
|
||||
import ProfileTab from './ProfileTab';
|
||||
import SoundsTab from './SoundsTab';
|
||||
/* eslint-enable lines-around-comment */
|
||||
import ShortcutsTab from './ShortcutsTab';
|
||||
import VirtualBackgroundTab from './VirtualBackgroundTab';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -46,7 +63,7 @@ interface IProps {
|
|||
/**
|
||||
* Information about the tabs to be rendered.
|
||||
*/
|
||||
_tabs: IDialogTab[];
|
||||
_tabs: IDialogTab<any>[];
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
|
@ -84,18 +101,10 @@ const styles = (theme: Theme) => {
|
|||
display: 'flex',
|
||||
width: '100%',
|
||||
|
||||
'&.profile-pane': {
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
'& .auth-name': {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
'& .calendar-tab, & .device-selection': {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .mock-atlaskit-label': {
|
||||
color: '#b8c7e0',
|
||||
fontSize: '12px',
|
||||
|
@ -130,19 +139,6 @@ const styles = (theme: Theme) => {
|
|||
width: '100%'
|
||||
},
|
||||
|
||||
'& .profile-edit': {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
padding: '0 2px',
|
||||
boxSizing: 'border-box'
|
||||
},
|
||||
|
||||
'& .profile-edit-field': {
|
||||
flex: 0.5,
|
||||
marginRight: '20px',
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
|
||||
'& .settings-sub-pane': {
|
||||
flex: 1
|
||||
},
|
||||
|
@ -165,11 +161,7 @@ const styles = (theme: Theme) => {
|
|||
|
||||
'& .settings-checkbox': {
|
||||
display: 'flex',
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
|
||||
'& .moderator-settings-wrapper': {
|
||||
paddingTop: '20px'
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
'& .calendar-tab': {
|
||||
|
@ -177,7 +169,8 @@ const styles = (theme: Theme) => {
|
|||
flexDirection: 'column',
|
||||
fontSize: '14px',
|
||||
minHeight: '100px',
|
||||
textAlign: 'center'
|
||||
textAlign: 'center',
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .calendar-tab-sign-in': {
|
||||
|
@ -194,11 +187,6 @@ const styles = (theme: Theme) => {
|
|||
},
|
||||
|
||||
'@media only screen and (max-width: 700px)': {
|
||||
'& .device-selection': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
'& .more-tab': {
|
||||
flexDirection: 'column'
|
||||
}
|
||||
|
@ -269,15 +257,18 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
const showCalendarSettings
|
||||
= configuredTabs.includes('calendar') && isCalendarEnabled(state);
|
||||
const showSoundsSettings = configuredTabs.includes('sounds');
|
||||
const tabs: IDialogTab[] = [];
|
||||
const enabledNotifications = getNotificationsMap(state);
|
||||
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
|
||||
const virtualBackgroundSupported = checkBlurSupport();
|
||||
const tabs: IDialogTab<any>[] = [];
|
||||
|
||||
if (showDeviceSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.DEVICES,
|
||||
component: DeviceSelection,
|
||||
labelKey: 'settings.devices',
|
||||
props: getDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
name: SETTINGS_TABS.AUDIO,
|
||||
component: AudioDevicesSelection,
|
||||
labelKey: 'settings.audio',
|
||||
props: getAudioDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getAudioDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
|
@ -286,14 +277,103 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
|
||||
return {
|
||||
...newProps,
|
||||
noiseSuppressionEnabled: tabState.noiseSuppressionEnabled,
|
||||
selectedAudioInputId: tabState.selectedAudioInputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} devices-pane`,
|
||||
submit: (newState: any) => submitAudioDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVolumeUp
|
||||
});
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.VIDEO,
|
||||
component: VideoDeviceSelection,
|
||||
labelKey: 'settings.video',
|
||||
props: getVideoDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getVideoDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
// tab would keep using a copy of the initial props it received,
|
||||
// leaving the device list to become stale.
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentFramerate: tabState?.currentFramerate,
|
||||
localFlipX: tabState.localFlipX,
|
||||
selectedVideoInputId: tabState.selectedVideoInputId
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} devices-pane`,
|
||||
submit: (newState: any) => submitDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVolumeUp
|
||||
submit: (newState: any) => submitVideoDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVideo
|
||||
});
|
||||
}
|
||||
|
||||
if (virtualBackgroundSupported) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.VIRTUAL_BACKGROUND,
|
||||
component: VirtualBackgroundTab,
|
||||
labelKey: 'virtualBackground.title',
|
||||
props: getVirtualBackgroundTabProps(state),
|
||||
className: `settings-pane ${classes.settingsDialog}`,
|
||||
submit: (newState: any) => submitVirtualBackgroundTab(newState),
|
||||
cancel: () => {
|
||||
const { _virtualBackground } = getVirtualBackgroundTabProps(state);
|
||||
|
||||
return submitVirtualBackgroundTab({
|
||||
options: {
|
||||
backgroundType: _virtualBackground.backgroundType,
|
||||
enabled: _virtualBackground.backgroundEffectEnabled,
|
||||
url: _virtualBackground.virtualSource,
|
||||
selectedThumbnail: _virtualBackground.selectedThumbnail,
|
||||
blurValue: _virtualBackground.blurValue
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
icon: IconImage
|
||||
});
|
||||
}
|
||||
|
||||
if (showSoundsSettings || showNotificationsSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.NOTIFICATIONS,
|
||||
component: NotificationsTab,
|
||||
labelKey: 'settings.notifications',
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getNotificationsTabProps>) => {
|
||||
return {
|
||||
...newProps,
|
||||
enabledNotifications: tabState?.enabledNotifications || {}
|
||||
};
|
||||
},
|
||||
props: getNotificationsTabProps(state, showSoundsSettings),
|
||||
className: `settings-pane ${classes.settingsDialog}`,
|
||||
submit: submitNotificationsTab,
|
||||
icon: IconBell
|
||||
});
|
||||
}
|
||||
|
||||
if (showModeratorSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MODERATOR,
|
||||
component: ModeratorTab,
|
||||
labelKey: 'settings.moderator',
|
||||
props: moderatorTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moderatorTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
followMeEnabled: tabState?.followMeEnabled,
|
||||
startAudioMuted: tabState?.startAudioMuted,
|
||||
startVideoMuted: tabState?.startVideoMuted,
|
||||
startReactionsMuted: tabState?.startReactionsMuted
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} moderator-pane`,
|
||||
submit: submitModeratorTab,
|
||||
icon: IconHost
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -309,29 +389,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
});
|
||||
}
|
||||
|
||||
if (showModeratorSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MODERATOR,
|
||||
component: ModeratorTab,
|
||||
labelKey: 'settings.moderator',
|
||||
props: moderatorTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
followMeEnabled: tabState?.followMeEnabled,
|
||||
startAudioMuted: tabState?.startAudioMuted,
|
||||
startVideoMuted: tabState?.startVideoMuted,
|
||||
startReactionsMuted: tabState?.startReactionsMuted
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} moderator-pane`,
|
||||
submit: submitModeratorTab,
|
||||
icon: IconModerator
|
||||
});
|
||||
}
|
||||
|
||||
if (showCalendarSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.CALENDAR,
|
||||
|
@ -342,36 +399,38 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
});
|
||||
}
|
||||
|
||||
if (showSoundsSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.SOUNDS,
|
||||
component: SoundsTab,
|
||||
labelKey: 'settings.sounds',
|
||||
props: getSoundsTabProps(state),
|
||||
className: `settings-pane ${classes.settingsDialog} profile-pane`,
|
||||
submit: submitSoundsTab,
|
||||
icon: IconBell
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.SHORTCUTS,
|
||||
component: ShortcutsTab,
|
||||
labelKey: 'settings.shortcuts',
|
||||
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getShortcutsTabProps>) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
keyboardShortcutsEnabled: tabState?.keyboardShortcutsEnabled
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog}`,
|
||||
submit: submitShortcutsTab,
|
||||
icon: IconShortcuts
|
||||
});
|
||||
|
||||
if (showMoreTab) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MORE,
|
||||
|
||||
// @ts-ignore
|
||||
component: MoreTab,
|
||||
labelKey: 'settings.more',
|
||||
props: moreTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moreTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentFramerate: tabState?.currentFramerate,
|
||||
currentLanguage: tabState?.currentLanguage,
|
||||
hideSelfView: tabState?.hideSelfView,
|
||||
showPrejoinPage: tabState?.showPrejoinPage,
|
||||
enabledNotifications: tabState?.enabledNotifications || {},
|
||||
maxStageParticipants: tabState?.maxStageParticipants
|
||||
};
|
||||
},
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
// @ts-expect-error
|
||||
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ShortcutsTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether to display the shortcuts or not.
|
||||
*/
|
||||
displayShortcuts: boolean;
|
||||
|
||||
/**
|
||||
* Wether the keyboard shortcuts are enabled or not.
|
||||
*/
|
||||
keyboardShortcutsEnabled: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
width: '100%',
|
||||
paddingBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
listContainer: {
|
||||
listStyleType: 'none',
|
||||
padding: 0,
|
||||
margin: 0
|
||||
},
|
||||
|
||||
listItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01
|
||||
},
|
||||
|
||||
listItemKey: {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
...withPixelLineHeight(theme.typography.labelBold),
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's profile.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ShortcutsTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code MoreTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
|
||||
this._renderShortcutsListItem = this._renderShortcutsListItem.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if global keyboard shortcuts
|
||||
* should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyboardShortcutEnableChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ keyboardShortcutsEnabled: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a keyboard shortcut with key and description.
|
||||
*
|
||||
* @param {string} keyboardKey - The keyboard key for the shortcut.
|
||||
* @param {string} translationKey - The translation key for the shortcut description.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
_renderShortcutsListItem(keyboardKey: string, translationKey: string) {
|
||||
const { classes, t } = this.props;
|
||||
let modifierKey = 'Alt';
|
||||
|
||||
if (window.navigator?.platform) {
|
||||
if (window.navigator.platform.indexOf('Mac') !== -1) {
|
||||
modifierKey = '⌥';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className = { classes.listItem }
|
||||
key = { keyboardKey }>
|
||||
<span
|
||||
aria-label = { t(translationKey) }>
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
<span className = { classes.listItemKey }>
|
||||
{keyboardKey.startsWith(':')
|
||||
? `${modifierKey} + ${keyboardKey.slice(1)}`
|
||||
: keyboardKey}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
displayShortcuts,
|
||||
keyboardShortcutsEnabled,
|
||||
t
|
||||
} = this.props;
|
||||
const shortcutDescriptions: Map<string, string> = displayShortcuts
|
||||
? keyboardShortcut.getShortcutsDescriptions()
|
||||
: new Map();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<Checkbox
|
||||
checked = { keyboardShortcutsEnabled }
|
||||
className = { classes.checkbox }
|
||||
label = { t('prejoin.keyboardShortcuts') }
|
||||
name = 'enable-keyboard-shortcuts'
|
||||
onChange = { this._onKeyboardShortcutEnableChanged } />
|
||||
{displayShortcuts && (
|
||||
<ul className = { classes.listContainer }>
|
||||
{Array.from(shortcutDescriptions)
|
||||
.map(description => this._renderShortcutsListItem(...description))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(translate(ShortcutsTab));
|
|
@ -1,169 +0,0 @@
|
|||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
// @ts-ignore
|
||||
import { AbstractDialogTab } from '../../../base/dialog';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SoundsTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
|
||||
/**
|
||||
* Whether or not the reactions feature is enabled.
|
||||
*/
|
||||
enableReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not moderator muted the sounds.
|
||||
*/
|
||||
moderatorMutedSoundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the incoming message should play.
|
||||
*/
|
||||
soundsIncomingMessage: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant joined should play.
|
||||
*/
|
||||
soundsParticipantJoined: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant entering the lobby should play.
|
||||
*/
|
||||
soundsParticipantKnocking: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant left should play.
|
||||
*/
|
||||
soundsParticipantLeft: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for reactions should play.
|
||||
*/
|
||||
soundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the talk while muted notification should play.
|
||||
*/
|
||||
soundsTalkWhileMuted: Boolean;
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's sound settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class SoundsTab extends AbstractDialogTab<Props> {
|
||||
/**
|
||||
* Initializes a new {@code SoundsTab} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code SoundsTab} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onChange = this._onChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a sound setting state.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChange({ target }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ [target.name]: target.checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
disabledSounds,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions,
|
||||
enableReactions,
|
||||
moderatorMutedSoundsReactions,
|
||||
t // @ts-ignore
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'sounds'>
|
||||
<h2 className = 'mock-atlaskit-label'>
|
||||
{t('settings.playSounds')}
|
||||
</h2>
|
||||
{enableReactions && <Checkbox
|
||||
checked = { soundsReactions && !disabledSounds.includes('REACTION_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { moderatorMutedSoundsReactions || disabledSounds.includes('REACTION_SOUND') }
|
||||
label = { t('settings.reactions') }
|
||||
name = 'soundsReactions'
|
||||
onChange = { this._onChange } />
|
||||
}
|
||||
<Checkbox
|
||||
checked = { soundsIncomingMessage && !disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
label = { t('settings.incomingMessage') }
|
||||
name = 'soundsIncomingMessage'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantJoined && !disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
label = { t('settings.participantJoined') }
|
||||
name = 'soundsParticipantJoined'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantLeft && !disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
label = { t('settings.participantLeft') }
|
||||
name = 'soundsParticipantLeft'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsTalkWhileMuted && !disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
label = { t('settings.talkWhileMuted') }
|
||||
name = 'soundsTalkWhileMuted'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantKnocking && !disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
className = 'settings-checkbox'
|
||||
disabled = { disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
label = { t('settings.participantKnocking') }
|
||||
name = 'soundsParticipantKnocking'
|
||||
onChange = { this._onChange } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default translate(SoundsTab);
|
|
@ -0,0 +1,107 @@
|
|||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps
|
||||
} from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import VirtualBackgrounds from '../../../virtual-background/components/VirtualBackgrounds';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VirtualBackgroundTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Returns the jitsi track that will have background effect applied.
|
||||
*/
|
||||
_jitsiTrack: Object;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Virtual background options.
|
||||
*/
|
||||
options: any;
|
||||
|
||||
/**
|
||||
* The selected thumbnail identifier.
|
||||
*/
|
||||
selectedThumbnail: string;
|
||||
}
|
||||
|
||||
const styles = () => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying language and moderator settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VirtualBackgroundTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code ModeratorTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onOptionsChanged = this._onOptionsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me mode
|
||||
* should be activated.
|
||||
*
|
||||
* @param {Object} options - The new background options.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOptionsChanged(options: any) {
|
||||
super._onChange({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
options,
|
||||
selectedThumbnail,
|
||||
_jitsiTrack
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'virtual-background-dialog'
|
||||
key = 'virtual-background'>
|
||||
<VirtualBackgrounds
|
||||
_jitsiTrack = { _jitsiTrack }
|
||||
onOptionsChange = { this._onOptionsChanged }
|
||||
options = { options }
|
||||
selectedThumbnail = { selectedThumbnail } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(translate(VirtualBackgroundTab));
|
|
@ -7,6 +7,7 @@ import { IconCheck, IconExclamationSolid } from '../../../../base/icons/svg';
|
|||
// @ts-ignore
|
||||
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import { TEXT_OVERFLOW_TYPES } from '../../../../base/ui/constants.any';
|
||||
|
||||
import Meter from './Meter';
|
||||
|
||||
|
@ -231,6 +232,7 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
|||
<ContextMenuItem
|
||||
accessibilityLabel = ''
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
text = { children }
|
||||
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
|
||||
|
|
|
@ -4,7 +4,7 @@ import React, { useRef } from 'react';
|
|||
import { IconCheck } from '../../../../base/icons/svg';
|
||||
import Button from '../../../../base/ui/components/web/Button';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.any';
|
||||
import { BUTTON_TYPES, TEXT_OVERFLOW_TYPES } from '../../../../base/ui/constants.any';
|
||||
import logger from '../../../logger';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.mp3';
|
||||
|
@ -118,6 +118,7 @@ const SpeakerEntry = (props: IProps) => {
|
|||
<ContextMenuItem
|
||||
accessibilityLabel = ''
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
text = { children }
|
||||
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { WithTranslation } from 'react-i18next';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { IconImage } from '../../../../base/icons/svg';
|
||||
import Video from '../../../../base/media/components/Video.web';
|
||||
|
@ -13,7 +12,8 @@ import Checkbox from '../../../../base/ui/components/web/Checkbox';
|
|||
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
|
||||
import { openSettingsDialog } from '../../../actions';
|
||||
import { SETTINGS_TABS } from '../../../constants';
|
||||
import { createLocalVideoTracks } from '../../../functions.web';
|
||||
|
||||
const videoClassName = 'video-preview-video flipVideoX';
|
||||
|
@ -297,7 +297,7 @@ const mapStateToProps = (state: IReduxState) => {
|
|||
|
||||
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
|
||||
return {
|
||||
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
|
||||
selectBackground: () => dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)),
|
||||
changeFlip: (flip: boolean) => {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: flip
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
export const SETTINGS_TABS = {
|
||||
AUDIO: 'audio_tab',
|
||||
CALENDAR: 'calendar_tab',
|
||||
DEVICES: 'devices_tab',
|
||||
MORE: 'more_tab',
|
||||
MODERATOR: 'moderator-tab',
|
||||
NOTIFICATIONS: 'notifications_tab',
|
||||
PROFILE: 'profile_tab',
|
||||
SOUNDS: 'sounds_tab'
|
||||
SHORTCUTS: 'shortcuts_tab',
|
||||
VIDEO: 'video_tab',
|
||||
VIRTUAL_BACKGROUND: 'virtual-background_tab'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @ts-expect-error
|
||||
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isNameReadOnly } from '../base/config/functions';
|
||||
|
@ -10,14 +12,14 @@ import {
|
|||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getHideSelfView } from '../base/settings/functions';
|
||||
import { getLocalVideoTrack } from '../base/tracks/functions.any';
|
||||
import { parseStandardURIString } from '../base/util/uri';
|
||||
import { isStageFilmstripEnabled } from '../filmstrip/functions';
|
||||
import { isFollowMeActive } from '../follow-me/functions';
|
||||
import { getParticipantsPaneConfig } from '../participants-pane/functions';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
|
||||
|
||||
/**
|
||||
* Used for web. Indicates if the setting section is enabled.
|
||||
*
|
||||
|
@ -115,25 +117,9 @@ export function getNotificationsMap(stateful: IStateful) {
|
|||
*/
|
||||
export function getMoreTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
|
||||
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
const enabledNotifications = getNotificationsMap(stateful);
|
||||
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
|
||||
|
||||
// when self view is controlled by the config we hide the settings
|
||||
const { disableSelfView, disableSelfViewSettings } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
currentFramerate: framerate,
|
||||
currentLanguage: language,
|
||||
desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
|
||||
disableHideSelfView: disableSelfViewSettings || disableSelfView,
|
||||
hideSelfView: getHideSelfView(state),
|
||||
languages: LANGUAGES,
|
||||
showLanguageSettings: configuredTabs.includes('language'),
|
||||
enabledNotifications,
|
||||
showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
|
||||
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
|
||||
showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled,
|
||||
maxStageParticipants: state['features/base/settings'].maxStageParticipants,
|
||||
|
@ -207,14 +193,25 @@ export function getProfileTabProps(stateful: IStateful) {
|
|||
} = state['features/base/conference'];
|
||||
const { hideEmailInSettings } = state['features/base/config'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// when self view is controlled by the config we hide the settings
|
||||
const { disableSelfView, disableSelfViewSettings } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
authEnabled: Boolean(conference && authEnabled),
|
||||
authLogin,
|
||||
disableHideSelfView: disableSelfViewSettings || disableSelfView,
|
||||
currentLanguage: language,
|
||||
displayName: localParticipant?.name,
|
||||
email: localParticipant?.email,
|
||||
hideEmailInSettings,
|
||||
hideSelfView: getHideSelfView(state),
|
||||
id: localParticipant?.id,
|
||||
languages: LANGUAGES,
|
||||
readOnlyName: isNameReadOnly(state),
|
||||
hideEmailInSettings
|
||||
showLanguageSettings: configuredTabs.includes('language')
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -224,10 +221,11 @@ export function getProfileTabProps(stateful: IStateful) {
|
|||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} showSoundsSettings - Whether to show the sound settings or not.
|
||||
* @returns {Object} - The properties for the "Sounds" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getSoundsTabProps(stateful: IStateful) {
|
||||
export function getNotificationsTabProps(stateful: IStateful, showSoundsSettings?: boolean) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
soundsIncomingMessage,
|
||||
|
@ -239,9 +237,12 @@ export function getSoundsTabProps(stateful: IStateful) {
|
|||
} = state['features/base/settings'];
|
||||
const enableReactions = isReactionsEnabled(state);
|
||||
const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false;
|
||||
const enabledNotifications = getNotificationsMap(stateful);
|
||||
|
||||
return {
|
||||
disabledSounds: state['features/base/config'].disabledSounds || [],
|
||||
enabledNotifications,
|
||||
showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
|
@ -249,7 +250,8 @@ export function getSoundsTabProps(stateful: IStateful) {
|
|||
soundsTalkWhileMuted,
|
||||
soundsReactions,
|
||||
enableReactions,
|
||||
moderatorMutedSoundsReactions
|
||||
moderatorMutedSoundsReactions,
|
||||
showSoundsSettings
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -272,3 +274,42 @@ export function getAudioSettingsVisibility(state: IReduxState) {
|
|||
export function getVideoSettingsVisibility(state: IReduxState) {
|
||||
return state['features/settings'].videoSettingsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Shortcuts" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the shortcuts dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePage?: boolean) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return {
|
||||
displayShortcuts: !isDisplayedOnWelcomePage && !isPrejoinPageVisible(state),
|
||||
keyboardShortcutsEnabled: keyboardShortcut.getEnabled()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Virtual Background" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getVirtualBackgroundTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return {
|
||||
_virtualBackground: state['features/virtual-background'],
|
||||
selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
|
||||
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
|
||||
};
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue