Compare commits

...

22 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
38068b33e5 fix(notifications) remove dead code 2023-02-14 10:11:08 +01:00
Robert Pintilii
22ded30b61 feat(audio-menu) Redesign audio picker menu (#12899)
Convert some files to TS
Remove unnecessary files
Implement redesign
Add noise suppression to picker menu
Fix Popover placement on browser resize
2023-02-13 16:01:08 +02:00
Robert Pintilii
533deea5fd ref(password-required) Update component to use new Dialog (#12900)
Convert component to TS
2023-02-13 15:47:42 +02:00
Saúl Ibarra Corretgé
46c6d1057d fix(ios) avoid getting duplicated SDK events
UIView has 2 designated initializers: initWithFrame and initWithCoder,
which means either of them is going to be called, whatever the
constructor.

THus overriding init will cause creating new (and unnecessary)
observers.

Ref: https://community.jitsi.org/t/duplicate-delegate-calls/121051/6
Fixes: https://github.com/jitsi/jitsi-meet/issues/12892
2023-02-13 14:20:44 +01:00
Titus Moldovan
45aa53b1a6 chore(rn) updates react-native-gesture-handler 2023-02-13 15:11:31 +02:00
damencho
7d65123495 fix: Drop unused dependency. 2023-02-13 06:48:53 +01:00
Maria Mironova
e1ac000cd1 fix(chat) keep avatar width inside flex container (#12891)
Co-authored-by: Maria Mironova <maria@example.com>
2023-02-10 14:31:11 +02:00
Saúl Ibarra Corretgé
f98036efa1 fixup! 2023-02-09 16:38:03 +01:00
Saúl Ibarra Corretgé
23aeafcc93 fixup! 2023-02-09 16:38:03 +01:00
Saúl Ibarra Corretgé
0ffe2c2c87 fixup! 2023-02-09 16:38:03 +01:00
Saúl Ibarra Corretgé
dec58afe46 feat(icons) add new moderator icon 2023-02-09 16:38:03 +01:00
Mihaela Dumitru
2aa770e532 feat(config) add flag to disable lobby password & group lobby config flags (#12793) 2023-02-09 14:46:25 +02:00
Gabriel Borlea
1a113ba733 feat: add custom buttons for participant menu and toolbar via config (#12832)
* add custom remote menu button

* add config for custom buttons

* whitelist custom buttons flag

* add toolbox custom button

* fix notify toolbox buttons

* whitelist toolbar custom buttons

* rename and fix notify

* rename participant remote menu

* revert some flag wrong changes

* fix some formatings

* add undefined type to custom buttons toolbox

* code review

* code review 2

* fix linting issue
2023-02-09 13:12:00 +02:00
dependabot[bot]
3a5833829c chore(deps): bump @sideway/formula from 3.0.0 to 3.0.1
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-09 10:36:48 +01:00
Priyanshu Sharma
e6d1f039d2 fix(participant-count) Fix mobile style (#12880) 2023-02-09 10:10:16 +02:00
Horatiu Muresan
fef78152e1 fix(remote-control) Add some missing key codes for numpad (#12879) 2023-02-07 17:00:59 +02:00
Mihaela Dumitru
84221c5c13 feat(dialout) check appId for permission to call outbound destination (#12871) 2023-02-07 16:18:27 +02:00
Calinteodor
3e59359563 feat(base/icon): removed burger icon (#12876)
* feat(base/icons): removed unused icon
2023-02-07 13:02:15 +02:00
Calin-Teodor
e69db9b878 feat(participants-pane): removed unused action 2023-02-07 12:40:35 +02:00
Robert Pintilii
ae7e441e21 fix(context-menu) Minor style fixes (#12874) 2023-02-07 12:05:46 +02:00
Robert Pintilii
6b8afbcceb fix(filmstrip) Minor style fixes (#12870) 2023-02-07 10:10:28 +02:00
José Luís Andrade
d712a565f8 lang: Update Portuguese translation (#12647)
* Update Portuguese translation

* Small fix in the translation

* Add new update

* feat(audioOnly) translation

* chore(welcome-page) translation

* translate "noMicPermission"
2023-02-03 11:41:11 -06:00
74 changed files with 957 additions and 841 deletions

View File

@@ -76,7 +76,6 @@ dependencies {
implementation project(':react-native-get-random-values')
implementation project(':react-native-immersive')
implementation project(':react-native-keep-awake')
implementation project(':react-native-masked-view_masked-view')
implementation project(':react-native-orientation-locker')
implementation project(':react-native-pager-view')
implementation project(':react-native-performance')

View File

@@ -119,7 +119,6 @@ class ReactInstanceManagerHolder {
new com.oblador.performance.PerformancePackage(),
new com.reactnativecommunity.slider.ReactSliderPackage(),
new com.brentvatne.react.ReactVideoPackage(),
new org.reactnative.maskedview.RNCMaskedViewPackage(),
new com.reactnativecommunity.webview.RNCWebViewPackage(),
new com.kevinresol.react_native_default_preference.RNDefaultPreferencePackage(),
new com.learnium.RNDeviceInfo.RNDeviceInfo(),

View File

@@ -542,12 +542,15 @@ var config = {
// Disables responsive tiles.
// disableResponsiveTiles: false,
// Hides lobby button
// DEPRECATED. Please use `securityUi?.hideLobbyButton` instead.
// Hides lobby button.
// hideLobbyButton: false,
// DEPRECATED. Please use `lobby?.autoKnock` instead.
// If Lobby is enabled starts knocking automatically.
// autoKnockLobby: false,
// DEPRECATED. Please use `lobby?.enableChat` instead.
// Enable lobby chat.
// enableLobbyChat: true,
@@ -572,6 +575,22 @@ var config = {
// customUrl: ''
// },
// Configs for the lobby screen.
// lobby {
// // If Lobby is enabled, it starts knocking automatically. Replaces `autoKnockLobby`.
// autoKnock: false,
// // Enables the lobby chat. Replaces `enableLobbyChat`.
// enableChat: true,
// },
// Configs for the security related UI elements.
// securityUi: {
// // Hides the lobby button. Replaces `hideLobbyButton`.
// hideLobbyButton: false,
// // Hides the possibility to set and enter a lobby password.
// disableLobbyPassword: false,
// },
// Disable app shortcuts that are registered upon joining a conference
// disableShortcuts: false,
@@ -799,6 +818,14 @@ var config = {
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [],
// An array with custom option buttons for the participant context menu
// type: Array<{ icon: string; id: string; text: string; }>
// customParticipantMenuButtons: [],
// An array with custom option buttons for the toolbar
// type: Array<{ icon: string; id: string; text: string; }>
// customToolbarButtons: [],
// Stats
//
@@ -1335,6 +1362,7 @@ var config = {
deploymentInfo
dialOutAuthUrl
dialOutCodesUrl
dialOutRegionUrl
disableRemoteControl
displayJids
externalConnectUrl

View File

@@ -2,13 +2,13 @@
display: inline-block;
&-content {
background: $menuBG;
border-radius: 3px;
font-size: 14px;
line-height: 24px;
position: relative;
right: auto;
margin-bottom: 8px;
max-height: 456px;
overflow: auto;
width: 300px;
&-ul {
margin:0;
padding:0;
@@ -16,90 +16,37 @@
}
}
&-header {
color: #fff;
align-items: center;
display: flex;
margin-top: 8px;
padding: 8px 16px;
&-icon {
display: inline-block;
svg {
fill: #fff;
}
}
&--bordered {
border-bottom: 1px solid #4C4D50;
}
&-text {
margin-left: 12px;
}
&-header:hover {
background-color: initial;
cursor: initial;
}
&-entry {
align-items: center;
color: #fff;
cursor: pointer;
display: flex;
padding: 8px 0;
margin-left: 48px;
&-entry-text {
display: inline-block;
text-overflow: ellipsis;
max-width: 213px;
overflow: hidden;
white-space: nowrap;
&--selected {
background: #131519;
cursor: initial;
margin-left: 0;
padding-left: 18px;
}
&-text {
color: #fff;
display: inline-block;
line-height: 24px;
text-overflow: ellipsis;
max-width: 213px;
overflow: hidden;
white-space: nowrap;
&.left-margin {
margin-left: 36px;
}
}
&-speaker {
position: relative;
&-ul {
margin:0;
padding:0;
list-style-type: none;
}
&:hover, &:focus-within, &:focus {
.audio-preview-entry {
background: #36383C;
margin-left: 0;
padding-left: 48px;
&--selected {
padding-left: 18px;
background: $newToolbarBackgroundColor;
}
}
.audio-preview-test-button {
display: inline-block;
}
.audio-preview-entry-text {
max-width: 178px;
margin-right: 0;
}
}
&:last-child {
padding-bottom: 8px;
}
.audio-preview-entry-text {
max-width: 238px;
}
@@ -108,19 +55,6 @@
&-microphone {
position: relative;
&:hover {
.audio-preview-entry {
background: #36383C;
margin-left: 0;
padding-left: 48px;
&--selected {
background: $newToolbarBackgroundColor;
padding-left: 18px;
}
}
}
&--nometer {
.audio-preview-entry-text {
max-width: 238px;
@@ -140,42 +74,21 @@
display: inline-block;
width: 14px;
& svg {
fill: #1C2025;
}
&--check {
background: #31B76A;
margin-right: 16px;
}
&--exclamation {
margin-left: 6px;
& svg {
fill: #E54B4B;
}
}
}
&-hr {
border-top: 1px solid #4C4D50;
border-bottom: 0;
}
&-test-button {
display: none;
background: #FFF;
border: 1px solid #D1DBE8;
border-radius: 3px;
color: #1C2025;
cursor: pointer;
font-weight: 600;
font-size: 0.8rem;
line-height: 24px;
padding: 2px 8px;
padding: 4px 10px;
position: absolute;
right: 16px;
top: 5px;
top: 6px;
}
&-meter-mic {
@@ -184,9 +97,7 @@
top: 14px;
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
outline: none;
padding: 0;
&-checkbox-container {
padding: 10px 16px;
}
}

View File

@@ -3,28 +3,28 @@
display: inline-block;
& > svg {
fill: #4E5E6C;
fill: #525252;
width: 38px;
}
}
&.metr--disabled {
& > svg {
fill: #4E5E6C;
fill: #525252;
}
}
}
.metr-l-0 {
rect:first-child {
fill: #31B76A;
fill: #1EC26A;
}
}
@for $i from 1 through 7 {
.metr-l-#{$i} {
rect:nth-child(-n+#{$i+1}) {
fill: #31B76A;
fill: #1EC26A;
}
}
}

View File

@@ -5,15 +5,15 @@
.popupmenu__contents {
.popupmenu__volume-slider {
&::-webkit-slider-runnable-track {
background-color: $popupSliderColor;
background-color: #246FE5;
}
&::-moz-range-track {
background-color: $popupSliderColor;
background-color: #246FE5;
}
&::-ms-fill-lower {
background-color: $popupSliderColor;
background-color: #246FE5;
}
}
}

View File

@@ -65,7 +65,6 @@ $errorColor: #c61600;
// Popover colors
$popoverFontColor: #ffffff !important;
$popupSliderColor: #0376da;
// Toolbar
$toolbarBackground: rgba(0, 0, 0, 0.5);

View File

@@ -469,7 +469,7 @@ PODS:
- React-Core
- RNDeviceInfo (8.4.8):
- React-Core
- RNGestureHandler (2.8.0):
- RNGestureHandler (2.9.0):
- React-Core
- RNGoogleSignin (7.0.4):
- GoogleSignIn (~> 6.0.0)
@@ -773,7 +773,7 @@ SPEC CHECKSUMS:
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f

View File

@@ -40,19 +40,10 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
#pragma mark Initializers
- (instancetype)init {
self = [super init];
if (self) {
[self initWithXXX];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self initWithXXX];
[self doInitialize];
}
return self;
@@ -61,7 +52,7 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initWithXXX];
[self doInitialize];
}
return self;
@@ -71,9 +62,9 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
* Internal initialization:
*
* - sets the background color
* - initializes the external API scope
* - registers necessary observers
*/
- (void)initWithXXX {
- (void)doInitialize {
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.

View File

@@ -89,7 +89,7 @@
"chat": {
"enter": "Entrar na sala",
"error": "Erro: a sua mensagem não foi enviada. Motivo: {{error}}",
"fieldPlaceHolder": "Escreva aqui a sua mensagem",
"fieldPlaceHolder": "Aa",
"lobbyChatMessageTo": "Mensagem de chat na sala de espera para {{recipient}}",
"message": "Mensagem",
"messageAccessibleTitle": "{{user}} disse:",
@@ -147,6 +147,7 @@
"bridgeCount": "Servidores: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Ligado a:",
"e2eeVerified": "E2EE verificada:",
"framerate": "Taxa de frames:",
"less": "Mostrar menos",
"localaddress": "Endereço local:",
@@ -266,7 +267,7 @@
"e2eeWarning": "AVISO: Nem todos os participantes neste encontro parecem ter apoio para a encriptação de ponta a ponta. Se o permitir, eles não o poderão ver nem ouvir.",
"e2eeWillDisableDueToMaxModeDescription": "AVISO: A encriptação de ponta a ponta será automaticamente desativada se mais participantes aderirem à conferência.",
"embedMeeting": "Embutir reunião",
"enterDisplayName": "Digite o seu nome aqui",
"enterDisplayName": "Digite o seu nome",
"error": "Erro",
"gracefulShutdown": "O nosso serviço está atualmente em manutenção. Por favor, tente novamente mais tarde.",
"grantModeratorDialog": "Tem a certeza que quer conceder direitos de moderador a {{participantName}}?",
@@ -408,6 +409,10 @@
"user": "Utilizador",
"userIdentifier": "Identificador do utilizador",
"userPassword": "Palavra-passe do utilizador",
"verifyParticipantConfirm": "Coincidem",
"verifyParticipantDismiss": "Não coincidem",
"verifyParticipantQuestion": "EXPERIMENTAL: Perguntar ao participante {{participantName}} se vêem o mesmo conteúdo, na mesma ordem.",
"verifyParticipantTitle": "Verificação pelo utilizador",
"videoLink": "Link do vídeo",
"viewUpgradeOptions": "Ver opções de actualização",
"viewUpgradeOptionsContent": "Para obter acesso ilimitado a funcionalidades premium como gravação, transcrições, RTMP Streaming & mais, terá de actualizar o seu plano.",
@@ -437,9 +442,6 @@
"noResults": "Não foram encontrados resultados :(",
"search": "Procurar no GIPHY"
},
"helpView": {
"title": "Centro de ajuda"
},
"incomingCall": {
"answer": "Responder",
"audioCallTitle": "Chamada recebida",
@@ -563,7 +565,6 @@
"lobby": {
"admit": "Aceitar",
"admitAll": "Aceitar todos",
"allow": "Permitir",
"backToKnockModeButton": "Peça para aderir",
"chat": "Chat",
"dialogTitle": "Modo sala de espera",
@@ -649,6 +650,8 @@
"connectedOneMember": "{{name}} entrou na reunião",
"connectedThreePlusMembers": "{{name}} e muitos outros entraram na reunião",
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
"dataChannelClosed": "Deficiência na qualidade do vídeo",
"dataChannelClosedDescription": "O canal de ponte foi desconectado e, portanto, a qualidade do vídeo está limitada à sua configuração mais baixa.",
"disconnected": "desconectado",
"displayNotifications": "Mostrar notificações para",
"focus": "Foco da conferência",
@@ -709,6 +712,8 @@
"reactionSoundsForAll": "Desativar sons para todos",
"screenShareNoAudio": "A caixa de compartilhar áudio não foi marcada no ecrã de seleção da janela.",
"screenShareNoAudioTitle": "Não foi possível partilhar o áudio do sistema!",
"screenSharingAudioOnlyDescription": "Note por favor que ao partilhar o seu ecrã está a afectar o modo \"Melhor desempenho\" e irá utilizar mais largura de banda.",
"screenSharingAudioOnlyTitle": "Modo \"Melhor desempenho\"",
"selfViewTitle": "Pode sempre reexibir a autovisualização a partir das definições",
"somebody": "Alguém",
"startSilentDescription": "Volte à reunião para habilitar o áudio",
@@ -858,9 +863,6 @@
"rejected": "Rejeitado",
"ringing": "Tocando..."
},
"privacyView": {
"title": "Privacidade"
},
"profile": {
"avatar": "avatar",
"setDisplayNameLabel": "Definir seu nome de exibição",
@@ -914,6 +916,7 @@
"localRecordingVideoWarning": "Para gravar o seu vídeo deve tê-lo ligado quando iniciar a gravação",
"localRecordingWarning": "Certifique-se de selecionar o separador actual a fim de utilizar o vídeo e áudio corretos. A gravação está actualmente limitada a 1 GB, o que é cerca de 100 minutos.",
"loggedIn": "Conectado como {{userName}}",
"noMicPermission": "Não foi possível criar a faixa de microfone. Por favor, conceda permissão para utilizar o microfone.",
"noStreams": "Não foi detetado nenhum sinal áudio ou vídeo.",
"off": "Gravação parada",
"offBy": "{{name}} parou a gravação",
@@ -964,7 +967,7 @@
"incomingMessage": "Receber uma mensagem",
"language": "Idioma",
"loggedIn": "Sessão iniciada como {{name}}",
"maxStageParticipants": "Número máximo de participantes que podem ser afixados",
"maxStageParticipants": "Número máximo de participantes que podem ser afixados (EXPERIMENTAL)",
"microphones": "Microfones",
"moderator": "Moderador",
"more": "Mais",
@@ -983,7 +986,7 @@
"sounds": "Sons",
"speakers": "Participantes",
"startAudioMuted": "Todos começam com microfone desligado",
"startReactionsMuted": "Sons de reação silenciados para todos",
"startReactionsMuted": "Todos começam com os sons de reação desativados",
"startVideoMuted": "Todos começam com câmara desligada",
"talkWhileMuted": "Falar com o microfone desligado",
"title": "Definições"
@@ -1003,6 +1006,7 @@
"displayName": "Nome de exibição",
"displayNamePlaceholderText": "Ex: João Dias",
"email": "Email",
"emailPlaceholderText": "email@example.com",
"goTo": "Ir para",
"header": "Configurações",
"help": "Ajuda",
@@ -1291,6 +1295,7 @@
"show": "Mostrar no palco",
"showSelfView": "Mostrar autovisualização",
"unpinFromStage": "Desafixar",
"verify": "Verificar participante",
"videoMuted": "Câmara desativada",
"videomute": "Participante parou a câmara"
},
@@ -1358,6 +1363,7 @@
"recentList": "Recente",
"recentListDelete": "Remover",
"recentListEmpty": "A sua lista recente está atualmente vazia. Converse com a sua equipa e encontrará aqui todas as suas reuniões recentes.",
"recentMeetings": "As suas reuniões recentes",
"reducedUIText": "Bem-vindo ao {{app}}!",
"roomNameAllowedChars": "Nome da reunião não deve conter qualquer um destes caracteres: ?. &, :, ', \", %, #.",
"roomname": "Digite o nome da sala",
@@ -1366,6 +1372,7 @@
"settings": "Definições",
"startMeeting": "Iniciar reunião",
"terms": "Termos",
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas"
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas",
"upcomingMeetings": "As suas próximas reuniões"
}
}

View File

@@ -1941,6 +1941,21 @@ class API {
});
}
/**
* Notify external application ( if API is enabled) that a participant menu button was clicked.
*
* @param {string} key - The key of the participant menu button.
* @param {string} participantId - The ID of the participant for with the participant menu button was clicked.
* @returns {void}
*/
notifyParticipantMenuButtonClicked(key, participantId) {
this._sendEvent({
name: 'participant-menu-button-clicked',
key,
participantId
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -140,6 +140,7 @@ const events = {
'raise-hand-updated': 'raiseHandUpdated',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'participant-menu-button-clicked': 'participantMenuButtonClick',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft',

26
package-lock.json generated
View File

@@ -94,7 +94,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.8.0",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
@@ -5654,9 +5654,9 @@
}
},
"node_modules/@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
@@ -16293,9 +16293,9 @@
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz",
"integrity": "sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz",
"integrity": "sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg==",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -24599,9 +24599,9 @@
}
},
"@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"@sideway/pinpoint": {
"version": "2.0.0",
@@ -32719,9 +32719,9 @@
"integrity": "sha512-MKbuBbovO8eGiAM9i6o0nrdBXivhRpzPQ+aVBXGJEPMH7RrCSNUKaCoEpkjfGHlTxjZimi6WjDCjjzCRSHlV1A=="
},
"react-native-gesture-handler": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz",
"integrity": "sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz",
"integrity": "sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg==",
"requires": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",

View File

@@ -99,7 +99,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.8.0",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",

View File

@@ -206,6 +206,8 @@ export interface IConfig {
};
};
corsAvatarURLs?: Array<string>;
customParticipantMenuButtons?: Array<{ icon: string; id: string; text: string; }>;
customToolbarButtons?: Array<{ icon: string; id: string; text: string; }>;
deeplinking?: IDeeplinkingConfig;
defaultLanguage?: string;
defaultLocalDisplayName?: string;
@@ -394,6 +396,10 @@ export interface IConfig {
validatorRegExpString?: string;
};
liveStreamingEnabled?: boolean;
lobby?: {
autoKnock?: boolean;
enableChat?: boolean;
};
localRecording?: {
disable?: boolean;
disableSelfRecording?: boolean;
@@ -464,6 +470,10 @@ export interface IConfig {
enabled?: boolean;
mode?: 'always' | 'recording';
};
securityUi?: {
disableLobbyPassword?: boolean;
hideLobbyButton?: boolean;
};
serviceUrl?: string;
sipInviteUrl?: string;
speakerStats?: {

View File

@@ -185,6 +185,7 @@ export default [
'inviteAppName',
'liveStreaming',
'liveStreamingEnabled',
'lobby',
'localRecording',
'localSubject',
'logging',
@@ -209,6 +210,7 @@ export default [
'resolution',
'salesforceUrl',
'screenshotCapture',
'securityUi',
'speakerStats',
'startAudioMuted',
'startAudioOnly',

View File

@@ -316,3 +316,13 @@ export function getDialOutStatusUrl(state: IReduxState) {
export function getDialOutUrl(state: IReduxState) {
return state['features/base/config'].guestDialOutUrl;
}
/**
* Selector to return the security UI config.
*
* @param {IReduxState} state - State object.
* @returns {Object}
*/
export function getSecurityUiConfig(state: IReduxState) {
return state['features/base/config']?.securityUi || {};
}

View File

@@ -32,9 +32,16 @@ export function getReplaceParticipant(state: IReduxState): string | undefined {
* @returns {Array<string>} - The list of enabled toolbar buttons.
*/
export function getToolbarButtons(state: IReduxState): Array<string> {
const { toolbarButtons } = state['features/base/config'];
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => id);
return Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
const buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
if (customButtons) {
buttons.push(...customButtons);
}
return buttons;
}
/**
@@ -101,3 +108,30 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
android.dynamicLink.isi = android.dynamicLink.isi || '1165103905';
}
}
/**
* Returns the list of buttons that have that notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Array} - The list of buttons.
*/
export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: string; preventExecution: boolean; }> {
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => {
return {
key: id,
preventExecution: false
};
});
const buttons = Array.isArray(buttonsWithNotifyClick)
? buttonsWithNotifyClick as Array<{ key: string; preventExecution: boolean; }>
: [];
if (customButtons) {
buttons.push(...customButtons);
}
return buttons;
}

View File

@@ -535,6 +535,30 @@ function _translateLegacyConfig(oldValue: IConfig) {
};
}
if (oldValue.autoKnockLobby !== undefined
&& newValue.lobby?.autoKnock === undefined) {
newValue.lobby = {
...newValue.lobby || {},
autoKnock: oldValue.autoKnockLobby
};
}
if (oldValue.enableLobbyChat !== undefined
&& newValue.lobby?.enableChat === undefined) {
newValue.lobby = {
...newValue.lobby || {},
enableChat: oldValue.enableLobbyChat
};
}
if (oldValue.hideLobbyButton !== undefined
&& newValue.securityUi?.hideLobbyButton === undefined) {
newValue.securityUi = {
...newValue.securityUi || {},
hideLobbyButton: oldValue.hideLobbyButton
};
}
_setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
return newValue;

View File

@@ -21,6 +21,7 @@ import StartRecordingDialog from '../../recording/components/Recording/web/Start
import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog';
// @ts-ignore
import RemoteControlAuthorizationDialog from '../../remote-control/components/RemoteControlAuthorizationDialog';
import PasswordRequiredPrompt from '../../room-lock/components/PasswordRequiredPrompt.web';
import SalesforceLinkDialog from '../../salesforce/components/web/SalesforceLinkDialog';
import ShareAudioDialog from '../../screen-share/components/web/ShareAudioDialog';
import ShareScreenWarningDialog from '../../screen-share/components/web/ShareScreenWarningDialog';
@@ -50,7 +51,7 @@ const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNam
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog,
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog ];
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog, PasswordRequiredPrompt ];
// This function is necessary while the transition from @atlaskit dialog to our component is ongoing.
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 6.75C1.5 6.33579 1.83579 6 2.25 6H21.75C22.1642 6 22.5 6.33579 22.5 6.75C22.5 7.16421 22.1642 7.5 21.75 7.5H2.25C1.83579 7.5 1.5 7.16421 1.5 6.75Z" />
<path d="M1.5 17.25C1.5 16.8358 1.83579 16.5 2.25 16.5H21.75C22.1642 16.5 22.5 16.8358 22.5 17.25C22.5 17.6642 22.1642 18 21.75 18H2.25C1.83579 18 1.5 17.6642 1.5 17.25Z" />
<path d="M2.25 11.25C1.83579 11.25 1.5 11.5858 1.5 12C1.5 12.4142 1.83579 12.75 2.25 12.75H21.75C22.1642 12.75 22.5 12.4142 22.5 12C22.5 11.5858 22.1642 11.25 21.75 11.25H2.25Z" />
</svg>

Before

Width:  |  Height:  |  Size: 623 B

View File

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" 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.6527C6.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" />
<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" />
<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" />
<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" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -20,7 +20,6 @@ export { default as IconCode } from './code.svg';
export { default as IconConnection } from './connection.svg';
export { default as IconConnectionInactive } from './ninja.svg';
export { default as IconCopy } from './copy.svg';
export { default as IconCrown } from './host.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDotsHorizontal } from './dots-horizontal.svg';
export { default as IconDownload } from './download.svg';
@@ -47,11 +46,11 @@ export { default as IconHelp } from './help.svg';
export { default as IconHighlight } from './highlight.svg';
export { default as IconImage } from './image.svg';
export { default as IconInfoCircle } from './info-circle.svg';
export { default as IconBurger } from './burger.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMeter } from './meter.svg';
export { default as IconMic } from './mic.svg';
export { default as IconMicSlash } from './mic-slash.svg';
export { default as IconModerator } from './moderator.svg';
export { default as IconNoiseSuppressionOff } from './noise-suppression-off.svg';
export { default as IconNoiseSuppressionOn } from './noise-suppression-on.svg';
export { default as IconArrowRight } from './arrow-right.svg';

View File

@@ -1,10 +1,10 @@
<svg width="38" height="12" viewBox="0 0 38 12" fill="#5E6D7A" xmlns="http://www.w3.org/2000/svg">
<rect width="3" height="12" rx="1"/>
<rect x="5" width="3" height="12" rx="1" />
<rect x="10" width="3" height="12" rx="1" />
<rect x="15" width="3" height="12" rx="1" />
<rect x="20" width="3" height="12" rx="1" />
<rect x="25" width="3" height="12" rx="1" />
<rect x="30" width="3" height="12" rx="1" />
<rect x="35" width="3" height="12" rx="1" />
<svg width="38" height="12" viewBox="0 0 38 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="3" height="12" rx="1" />
<rect x="5" width="3" height="12" rx="1" />
<rect x="10" width="3" height="12" rx="1" />
<rect x="15" width="3" height="12" rx="1" />
<rect x="20" width="3" height="12" rx="1" />
<rect x="25" width="3" height="12" rx="1" />
<rect x="30" width="3" height="12" rx="1" />
<rect x="35" width="3" height="12" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 3H19.5C20.3284 3 21 3.67157 21 4.5V19.5C21 20.3284 20.3284 21 19.5 21H4.5C3.67157 21 3 20.3284 3 19.5V4.5C3 3.67157 3.67157 3 4.5 3ZM1.5 4.5C1.5 2.84315 2.84315 1.5 4.5 1.5H19.5C21.1569 1.5 22.5 2.84315 22.5 4.5V19.5C22.5 21.1569 21.1569 22.5 19.5 22.5H4.5C2.84315 22.5 1.5 21.1569 1.5 19.5V4.5ZM8.71837 7H6.30005V17.9091H8.19636V10.3984H8.29756L11.3125 17.8771H12.7294L15.7443 10.4144H15.8455V17.9091H17.7418V7H15.3235L12.0849 14.9048H11.957L8.71837 7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@@ -52,7 +52,7 @@ const DEFAULT_STATE: ISettingsState = {
};
export interface ISettingsState {
audioOutputDeviceId?: string | boolean;
audioOutputDeviceId?: string;
audioSettingsVisible?: boolean;
avatarURL?: string;
cameraDeviceId?: string | boolean;
@@ -108,6 +108,7 @@ Object.keys(DEFAULT_STATE).forEach(key => {
// we want to filter these props, to not be stored as they represent
// what is currently opened/used as devices
// @ts-ignore
filterSubtree.audioOutputDeviceId = false;
filterSubtree.cameraDeviceId = false;
filterSubtree.micDeviceId = false;

View File

@@ -34,6 +34,8 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps {
[key: `aria-${string}`]: string;
/**
* Accessibility label for menu container.
*/
@@ -59,6 +61,11 @@ interface IProps {
*/
hidden?: boolean;
/**
* Optional id.
*/
id?: string;
/**
* Whether or not the menu is already in a drawer.
*/
@@ -98,6 +105,11 @@ interface IProps {
* Callback for the mouse leaving the component.
*/
onMouseLeave?: (e?: React.MouseEvent) => void;
/**
* Tab index for the menu.
*/
tabIndex?: number;
}
const MAX_HEIGHT = 400;
@@ -108,7 +120,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui01,
border: `1px solid ${theme.palette.ui04}`,
borderRadius: `${Number(theme.shape.borderRadius)}px`,
boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
marginTop: `${(participantsPaneTheme.panePadding * 2) + theme.typography.bodyShortRegular.fontSize}px`,
@@ -146,6 +158,7 @@ const ContextMenu = ({
className,
entity,
hidden,
id,
inDrawer,
isDrawerOpen,
offsetTarget,
@@ -153,7 +166,8 @@ const ContextMenu = ({
onKeyDown,
onDrawerClose,
onMouseEnter,
onMouseLeave
onMouseLeave,
tabIndex
}: IProps) => {
const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -217,11 +231,14 @@ const ContextMenu = ({
isHidden && styles.contextMenuHidden,
className
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }>
ref = { containerRef }
role = 'menu'
tabIndex = { tabIndex }>
{children}
</div>;
};

View File

@@ -13,6 +13,11 @@ export interface IProps {
*/
accessibilityLabel: string;
/**
* Component children.
*/
children?: ReactNode;
/**
* CSS class name used for custom styles.
*/
@@ -54,6 +59,11 @@ export interface IProps {
*/
onKeyPress?: (e?: React.KeyboardEvent) => void;
/**
* Whether the item is marked as selected.
*/
selected?: boolean;
/**
* TestId of the element, if any.
*/
@@ -62,7 +72,7 @@ export interface IProps {
/**
* Action text.
*/
text: string;
text?: string;
/**
* Class name for the text.
@@ -90,9 +100,19 @@ const useStyles = makeStyles()(theme => {
'&:active': {
backgroundColor: theme.palette.ui03
},
'&:focus': {
boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}`
}
},
selected: {
borderLeft: `3px solid ${theme.palette.action01Hover}`,
paddingLeft: '13px',
backgroundColor: theme.palette.ui02
},
contextMenuItemDisabled: {
pointerEvents: 'none'
},
@@ -120,6 +140,7 @@ const useStyles = makeStyles()(theme => {
const ContextMenuItem = ({
accessibilityLabel,
children,
className,
customIcon,
disabled,
@@ -128,6 +149,7 @@ const ContextMenuItem = ({
onClick,
onKeyDown,
onKeyPress,
selected,
testId,
text,
textClassName }: IProps) => {
@@ -141,6 +163,7 @@ const ContextMenuItem = ({
className = { cx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
disabled && styles.contextMenuItemDisabled,
selected && styles.selected,
className
) }
data-testid = { testId }
@@ -148,13 +171,15 @@ const ContextMenuItem = ({
key = { text }
onClick = { disabled ? undefined : onClick }
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPress }>
onKeyPress = { disabled ? undefined : onKeyPress }
role = 'menuitem'>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
<span className = { cx(textClassName) }>{text}</span>
{text && <span className = { cx(styles.text, textClassName) }>{text}</span>}
{children}
</div>
);
};

View File

@@ -25,7 +25,7 @@ const useStyles = makeStyles()(theme => {
},
'& + &:not(:empty)': {
borderTop: `1px solid ${theme.palette.ui04}`
borderTop: `1px solid ${theme.palette.ui03}`
},
'&:first-of-type': {

View File

@@ -45,6 +45,7 @@ const useStyles = makeStyles()(theme => {
avatar: {
margin: `${theme.spacing(1)} ${theme.spacing(2)} ${theme.spacing(3)} 0`,
position: 'sticky',
flexShrink: 0,
top: 0
}
};

View File

@@ -147,7 +147,7 @@ const styles = (theme: Theme) => {
},
icon: {
padding: '6px',
padding: '4px',
borderRadius: '4px',
'&.status-high': {

View File

@@ -103,7 +103,7 @@ export const ConnectionIndicatorIcon = ({
<span className = { emptyIconWrapperClassName }>
<Icon
className = { clsx(classes.icon, colorClass) }
size = { 12 }
size = { 16 }
src = { IconConnection } />
</span>
);

View File

@@ -1,22 +0,0 @@
// @flow
import React, { Component } from 'react';
import { IconCrown } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
* Thumbnail badge showing that the participant is a conference moderator.
*/
export default class ModeratorIndicator extends Component<{}> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<BaseIndicator icon = { IconCrown } />
);
}
}

View File

@@ -0,0 +1,17 @@
/* eslint-disable lines-around-comment */
import React from 'react';
// @ts-ignore
import { IconModerator } from '../../../base/icons';
// @ts-ignore
import { BaseIndicator } from '../../../base/react';
/**
* Thumbnail badge showing that the participant is a conference moderator.
*
* @returns {JSX.Element}
*/
const ModeratorIndicator = (): JSX.Element => <BaseIndicator icon = { IconModerator } />;
export default ModeratorIndicator;

View File

@@ -853,6 +853,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
{ ...actions }>
<Icon
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
size = { 24 }
src = { icon } />
</button>
</div>

View File

@@ -1,8 +1,10 @@
/* @flow */
/* eslint-disable lines-around-comment */
import React from 'react';
import { IconCrown } from '../../../base/icons';
// @ts-ignore
import { IconModerator } from '../../../base/icons';
// @ts-ignore
import { BaseIndicator } from '../../../base/react';
/**
@@ -13,17 +15,17 @@ type Props = {
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
tooltipPosition: string;
};
/**
* React {@code Component} for showing a moderator icon with a tooltip.
*
* @returns {Component}
* @returns {JSX.Element}
*/
const ModeratorIndicator = ({ tooltipPosition }: Props) => (
const ModeratorIndicator = ({ tooltipPosition }: Props): JSX.Element => (
<BaseIndicator
icon = { IconCrown }
icon = { IconModerator }
iconSize = { 16 }
tooltipKey = 'videothumbnail.moderator'
tooltipPosition = { tooltipPosition } />

View File

@@ -337,7 +337,7 @@ const defaultStyles = (theme: Theme) => {
activeSpeaker: {
'& .active-speaker-indicator': {
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
boxShadow: `inset 0px 0px 0px 3px ${theme.palette.action01Hover} !important`
}
},

View File

@@ -20,7 +20,7 @@ export const styles = (theme: Theme) => {
height: '24px',
position: 'absolute' as const,
borderRadius: '4px',
top: 'calc(-24px - 3px)',
top: 'calc(-24px - 2px)',
left: 'calc(50% - 16px)',
opacity: 0,
transition: 'opacity .3s',
@@ -51,7 +51,7 @@ export const styles = (theme: Theme) => {
toggleVerticalFilmstripContainer: {
transform: 'rotate(-90deg)',
left: 'calc(-24px - 3px - 4px)',
left: 'calc(-24px - 2px - 4px)',
top: 'calc(50% - 12px)'
},

View File

@@ -35,6 +35,11 @@ export type Props = {
*/
_dialOutAuthUrl: string,
/**
* The URL for validating if an outbound destination is allowed.
*/
_dialOutRegionUrl: string;
/**
* Whether or not to show Dial Out functionality.
*/
@@ -235,7 +240,9 @@ export default class AbstractAddPeopleDialog<P: Props, S: State>
_query(query = '') {
const {
_addPeopleEnabled: addPeopleEnabled,
_appId: appId,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutRegionUrl: dialOutRegionUrl,
_dialOutEnabled: dialOutEnabled,
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
@@ -244,8 +251,10 @@ export default class AbstractAddPeopleDialog<P: Props, S: State>
} = this.props;
const options = {
addPeopleEnabled,
appId,
dialOutAuthUrl,
dialOutEnabled,
dialOutRegionUrl,
jwt,
peopleSearchQueryTypes,
peopleSearchUrl,
@@ -275,14 +284,17 @@ export function _mapStateToProps(state: Object) {
const {
callFlowsEnabled,
dialOutAuthUrl,
dialOutRegionUrl,
peopleSearchQueryTypes,
peopleSearchUrl
} = state['features/base/config'];
return {
_addPeopleEnabled: isAddPeopleEnabled(state),
_appId: state['features/base/jwt']?.tenant,
_callFlowsEnabled: callFlowsEnabled,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutRegionUrl: dialOutRegionUrl,
_dialOutEnabled: isDialOutEnabled(state),
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,

View File

@@ -8,6 +8,7 @@ import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { doGetJSON } from '../base/util/httpUtils';
import { parseURLParams } from '../base/util/parseURLParams';
import {
StatusCode,
@@ -55,6 +56,34 @@ export function checkDialNumber(
});
}
/**
* Sends an ajax request to check if the outbound call is permitted.
*
* @param {string} dialOutRegionUrl - The config endpoint.
* @param {string} jwt - The jwt token.
* @param {string} appId - The customer id.
* @param {string} phoneNumber - The destination phone number.
* @returns {Promise} - The promise created by the request.
*/
export function checkOutboundDestination(
dialOutRegionUrl: string,
jwt: string,
appId: string,
phoneNumber: string
): Promise<any> {
return doGetJSON(dialOutRegionUrl, true, {
body: JSON.stringify({
appId,
phoneNumber
}),
method: 'POST',
headers: {
'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json'
}
});
}
/**
* Removes all non-numeric characters from a string.
*
@@ -76,6 +105,11 @@ export type GetInviteResultsOptions = {
*/
addPeopleEnabled: boolean;
/**
* The customer id.
*/
appId: string;
/**
* The endpoint to use for checking phone number validity.
*/
@@ -86,6 +120,11 @@ export type GetInviteResultsOptions = {
*/
dialOutEnabled: boolean;
/**
* The endpoint to use for checking dial permission to an outbound destination.
*/
dialOutRegionUrl: string;
/**
* The jwt token to pass to the search service.
*/
@@ -123,8 +162,10 @@ export function getInviteResultsForQuery(
const text = query.trim();
const {
dialOutAuthUrl,
addPeopleEnabled,
appId,
dialOutAuthUrl,
dialOutRegionUrl,
dialOutEnabled,
peopleSearchQueryTypes,
peopleSearchUrl,
@@ -187,7 +228,7 @@ export function getInviteResultsForQuery(
}
return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
.then(([ peopleResults, phoneResults ]) => {
.then(async ([ peopleResults, phoneResults ]) => {
const results: any[] = [
...peopleResults
];
@@ -203,14 +244,26 @@ export function getInviteResultsForQuery(
= peopleResults.find(result => result.type === INVITE_TYPES.PHONE);
if (!hasPhoneResult && typeof phoneResults.allow === 'boolean') {
results.push({
const result = {
allowed: phoneResults.allow,
country: phoneResults.country,
type: INVITE_TYPES.PHONE,
number: phoneResults.phone,
originalEntry: text,
showCountryCodeReminder: !hasCountryCode
});
};
if (!phoneResults.allow) {
try {
const response = await checkOutboundDestination(dialOutRegionUrl, jwt, appId, text);
result.allowed = response.allowed;
} catch (error) {
logger.error('Error checking permission to dial to outbound destination', error);
}
}
results.push(result);
}
if (sipInviteEnabled && isASipAddress(text)) {

View File

@@ -20,7 +20,7 @@ import {
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
import { LOBBY_CHAT_INITIALIZED, MODERATOR_IN_CHAT_WITH_LEFT } from './constants';
import { getKnockingParticipants, getLobbyEnabled } from './functions';
import { getKnockingParticipants, getLobbyConfig, getLobbyEnabled } from './functions';
import { IKnockingParticipant } from './types';
/**
@@ -389,9 +389,9 @@ export function setLobbyMessageListener() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const conference = getCurrentConference(state);
const { enableLobbyChat = true } = state['features/base/config'];
const { enableChat = true } = getLobbyConfig(state);
if (!enableLobbyChat) {
if (!enableChat) {
return;
}

View File

@@ -3,6 +3,7 @@
import React, { PureComponent } from 'react';
import { conferenceWillJoin, getConferenceName } from '../../base/conference';
import { getSecurityUiConfig } from '../../base/config/functions.any';
import { INVITE_ENABLED, getFeatureFlag } from '../../base/flags';
import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
@@ -443,6 +444,7 @@ export function _mapStateToProps(state: Object): $Shape<Props> {
const { disableInviteFunctions } = state['features/base/config'];
const { knocking, passwordJoinFailed } = state['features/lobby'];
const { iAmSipGateway } = state['features/base/config'];
const { disableLobbyPassword } = getSecurityUiConfig(state);
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
const deviceStatusVisible = isDeviceStatusVisible(state);
const { membersOnly } = state['features/base/conference'];
@@ -460,7 +462,7 @@ export function _mapStateToProps(state: Object): $Shape<Props> {
_participantId: participantId,
_participantName: localParticipant?.name,
_passwordJoinFailed: passwordJoinFailed,
_renderPassword: !iAmSipGateway,
_renderPassword: !iAmSipGateway && !disableLobbyPassword,
showCopyUrlButton
};
}

View File

@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState } from '../../../app/types';
import { getSecurityUiConfig } from '../../../base/config/functions.any';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { connect } from '../../../base/redux/functions';
@@ -83,25 +84,22 @@ class LobbySection extends PureComponent<IProps, IState> {
}
return (
<>
<div id = 'lobby-section'>
<p
className = 'description'
role = 'banner'>
{ t('lobby.enableDialogText') }
</p>
<div className = 'control-row'>
<label htmlFor = 'lobby-section-switch'>
{ t('lobby.toggleLabel') }
</label>
<Switch
checked = { this.state.lobbyEnabled }
id = 'lobby-section-switch'
onChange = { this._onToggleLobby } />
</div>
<div id = 'lobby-section'>
<p
className = 'description'
role = 'banner'>
{ t('lobby.enableDialogText') }
</p>
<div className = 'control-row'>
<label htmlFor = 'lobby-section-switch'>
{ t('lobby.toggleLabel') }
</label>
<Switch
checked = { this.state.lobbyEnabled }
id = 'lobby-section-switch'
onChange = { this._onToggleLobby } />
</div>
<div className = 'separator-line' />
</>
</div>
);
}
@@ -129,7 +127,7 @@ class LobbySection extends PureComponent<IProps, IState> {
*/
function mapStateToProps(state: IReduxState): Partial<IProps> {
const { conference } = state['features/base/conference'];
const { hideLobbyButton } = state['features/base/config'];
const { hideLobbyButton } = getSecurityUiConfig(state);
return {
_lobbyEnabled: state['features/lobby'].lobbyEnabled,

View File

@@ -44,6 +44,15 @@ export function getKnockingParticipantsById(state: IReduxState) {
return getKnockingParticipants(state).map(participant => participant.id);
}
/**
* Selector to return the lobby config.
*
* @param {IReduxState} state - State object.
* @returns {Object}
*/
export function getLobbyConfig(state: IReduxState) {
return state['features/base/config']?.lobby || {};
}
/**
* Function that handles the visibility of the lobby chat message.
@@ -56,13 +65,13 @@ export function showLobbyChatButton(
) {
return function(state: IReduxState) {
const { enableLobbyChat = true } = state['features/base/config'];
const { enableChat = true } = getLobbyConfig(state);
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
const conference = getCurrentConference(state);
const lobbyLocalId = conference?.myLobbyUserId();
if (!enableLobbyChat) {
if (!enableChat) {
return false;
}

View File

@@ -124,7 +124,6 @@ const useStyles = (theme: Theme) => {
*/
class NotificationsContainer extends Component<IProps> {
_api: Object;
_timeouts: Map<string, number>;
/**
* Initializes a new {@code NotificationsContainer} instance.
@@ -134,8 +133,6 @@ class NotificationsContainer extends Component<IProps> {
constructor(props: IProps) {
super(props);
this._timeouts = new Map();
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
@@ -186,13 +183,6 @@ class NotificationsContainer extends Component<IProps> {
* @returns {void}
*/
_onDismissed(uid: string) {
const timeout = this._timeouts.get(`${uid}`);
if (timeout) {
clearTimeout(timeout);
this._timeouts.delete(`${uid}`);
}
this.props.dispatch(hideNotification(uid));
}

View File

@@ -12,24 +12,9 @@ import ConnectionStatusComponent
import RemoteVideoMenu from '../video-menu/components/native/RemoteVideoMenu';
import { SET_VOLUME } from './actionTypes';
import {
ContextMenuLobbyParticipantReject
// @ts-ignore
} from './components/native';
import RoomParticipantMenu from './components/native/RoomParticipantMenu';
export * from './actions.any';
/* eslint-enable lines-around-comment */
/**
* Displays the context menu for the selected lobby participant.
*
* @param {Object} participant - The selected lobby participant.
* @returns {Function}
*/
export function showContextMenuReject(participant: Object) {
return openSheet(ContextMenuLobbyParticipantReject, { participant });
}
/**
* Displays the connection status for the local meeting participant.

View File

@@ -25,6 +25,7 @@ import {
getParticipantCount,
isEveryoneModerator
} from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
@@ -45,6 +46,7 @@ const useStyles = makeStyles()(theme => {
},
text: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text02,
padding: '10px 16px',
height: '40px',

View File

@@ -11,13 +11,16 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui03,
borderRadius: '100%',
height: '16px',
width: '16px',
minWidth: '16px',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.labelBold),
pointerEvents: 'none',
position: 'absolute',
right: '-4px',
top: '-3px'
top: '-3px',
textAlign: 'center',
boxSizing: 'border-box',
paddingTop: '2px'
}
};
});

View File

@@ -2,6 +2,7 @@ import { IReduxState } from '../app/types';
import { getRoomName } from '../base/conference/functions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media/functions';
import { getLobbyConfig } from '../lobby/functions';
/**
* Selector for the visibility of the 'join by phone' button.
@@ -159,11 +160,12 @@ export function isPrejoinPageVisible(state: IReduxState): boolean {
* @returns {boolean}
*/
export function shouldAutoKnock(state: IReduxState): boolean {
const { iAmRecorder, iAmSipGateway, autoKnockLobby, prejoinConfig } = state['features/base/config'];
const { iAmRecorder, iAmSipGateway, prejoinConfig } = state['features/base/config'];
const { userSelectedSkipPrejoin } = state['features/base/settings'];
const { autoKnock } = getLobbyConfig(state);
const isPrejoinEnabled = prejoinConfig?.enabled;
return Boolean(((isPrejoinEnabled && !userSelectedSkipPrejoin)
|| autoKnockLobby || (iAmRecorder && iAmSipGateway))
|| autoKnock || (iAmRecorder && iAmSipGateway))
&& !state['features/lobby'].knocking);
}

View File

@@ -67,7 +67,9 @@ export const KEYS = {
BACKSLASH: '\\',
MINUS: '-',
EQUAL: '=',
SLASH: '/'
SLASH: '/',
ASTERISK: '*',
PLUS: '+'
};
/* eslint-disable max-len */
@@ -114,6 +116,11 @@ const keyCodeToKey = {
103: KEYS.NUMPAD_7,
104: KEYS.NUMPAD_8,
105: KEYS.NUMPAD_9,
106: KEYS.ASTERISK,
107: KEYS.PLUS,
109: KEYS.MINUS,
110: KEYS.PERIOD,
111: KEYS.SLASH,
112: KEYS.F1,
113: KEYS.F2,
114: KEYS.F3,

View File

@@ -1,11 +1,12 @@
// @flow
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { setPassword } from '../../base/conference';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { IStore } from '../../app/types';
import { setPassword } from '../../base/conference/actions';
import { IJitsiConference } from '../../base/conference/reducer';
import { translate } from '../../base/i18n/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import Input from '../../base/ui/components/web/Input';
import { _cancelPasswordRequiredPrompt } from '../actions';
@@ -13,23 +14,18 @@ import { _cancelPasswordRequiredPrompt } from '../actions';
* The type of the React {@code Component} props of
* {@link PasswordRequiredPrompt}.
*/
type Props = {
interface IProps extends WithTranslation {
/**
* The JitsiConference which requires a password.
*/
conference: Object,
conference: IJitsiConference;
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<any>,
/**
* The translate function.
*/
t: Function
};
dispatch: IStore['dispatch'];
}
/**
* The type of the React {@code Component} state of
@@ -40,14 +36,14 @@ type State = {
/**
* The password entered by the local participant.
*/
password: string
}
password?: string;
};
/**
* Implements a React Component which prompts the user when a password is
* required to join a conference.
*/
class PasswordRequiredPrompt extends Component<Props, State> {
class PasswordRequiredPrompt extends Component<IProps, State> {
state = {
password: ''
};
@@ -58,7 +54,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
* @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 handlers so they are only bound once per instance.
@@ -76,12 +72,10 @@ class PasswordRequiredPrompt extends Component<Props, State> {
render() {
return (
<Dialog
disableBlanketClickDismiss = { true }
isModal = { false }
disableBackdropClose = { true }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'dialog.passwordRequired'
width = 'small'>
titleKey = 'dialog.passwordRequired'>
{ this._renderBody() }
</Dialog>
);
@@ -98,6 +92,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
<div>
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
label = { this.props.t('dialog.passwordLabel') }
name = 'lockKey'
onChange = { this._onPasswordChanged }
@@ -107,8 +102,6 @@ class PasswordRequiredPrompt extends Component<Props, State> {
);
}
_onPasswordChanged: ({ target: { value: * }}) => void;
/**
* Notifies this dialog that password has changed.
*
@@ -122,8 +115,6 @@ class PasswordRequiredPrompt extends Component<Props, State> {
});
}
_onCancel: () => boolean;
/**
* Dispatches action to cancel and dismiss this dialog.
*
@@ -138,8 +129,6 @@ class PasswordRequiredPrompt extends Component<Props, State> {
return true;
}
_onSubmit: () => boolean;
/**
* Dispatches action to submit value from this dialog.
*

View File

@@ -3,6 +3,7 @@
import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { getSecurityUiConfig } from '../../../base/config/functions.any';
import {
LOBBY_MODE_ENABLED,
MEETING_PASSWORD_ENABLED,
@@ -81,7 +82,7 @@ export default class AbstractSecurityDialogButton<P: Props, S:*>
*/
export function _mapStateToProps(state: Object) {
const { conference } = state['features/base/conference'];
const { hideLobbyButton } = state['features/base/config'];
const { hideLobbyButton } = getSecurityUiConfig(state);
const { locked } = state['features/base/conference'];
const { lobbyEnabled } = state['features/lobby'];
const lobbySupported = conference && conference.isLobbySupported();

View File

@@ -5,6 +5,7 @@ import {
} from 'react-native';
import type { Dispatch } from 'redux';
import { getSecurityUiConfig } from '../../../../base/config/functions.any';
import { MEETING_PASSWORD_ENABLED, getFeatureFlag } from '../../../../base/flags';
import { translate } from '../../../../base/i18n';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
@@ -502,7 +503,7 @@ class SecurityDialog extends PureComponent<Props, State> {
*/
function _mapStateToProps(state: Object): Object {
const { conference, locked, password } = state['features/base/conference'];
const { hideLobbyButton } = state['features/base/config'];
const { disableLobbyPassword, hideLobbyButton } = getSecurityUiConfig(state);
const { lobbyEnabled } = state['features/lobby'];
const { roomPasswordNumberOfDigits } = state['features/base/config'];
const lobbySupported = conference && conference.isLobbySupported();
@@ -518,7 +519,7 @@ function _mapStateToProps(state: Object): Object {
_lockedConference: Boolean(conference && locked),
_password: password,
_passwordNumberOfDigits: roomPasswordNumberOfDigits,
_roomPasswordControls: visible
_roomPasswordControls: visible && !disableLobbyPassword
};
}

View File

@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import { IReduxState } from '../../../../app/types';
import { setPassword as setPass } from '../../../../base/conference/actions';
import { getSecurityUiConfig } from '../../../../base/config/functions.any';
import { isLocalParticipantModerator } from '../../../../base/participants/functions';
import { connect } from '../../../../base/redux/functions';
import Dialog from '../../../../base/ui/components/web/Dialog';
@@ -37,6 +38,11 @@ interface IProps {
*/
_conference: Object;
/**
* Whether to hide the lobby password section.
*/
_disableLobbyPassword?: boolean;
/**
* The value for how the conference is locked (or undefined if not locked)
* as defined by room-lock constants.
@@ -73,6 +79,7 @@ function SecurityDialog({
_buttonsWithNotifyClick,
_canEditPassword,
_conference,
_disableLobbyPassword,
_locked,
_password,
_passwordNumberOfDigits,
@@ -94,16 +101,21 @@ function SecurityDialog({
titleKey = 'security.title'>
<div className = 'security-dialog'>
<LobbySection />
<PasswordSection
buttonsWithNotifyClick = { _buttonsWithNotifyClick }
canEditPassword = { _canEditPassword }
conference = { _conference }
locked = { _locked }
password = { _password }
passwordEditEnabled = { passwordEditEnabled }
passwordNumberOfDigits = { _passwordNumberOfDigits }
setPassword = { setPassword }
setPasswordEditEnabled = { setPasswordEditEnabled } />
{!_disableLobbyPassword && (
<>
<div className = 'separator-line' />
<PasswordSection
buttonsWithNotifyClick = { _buttonsWithNotifyClick }
canEditPassword = { _canEditPassword }
conference = { _conference }
locked = { _locked }
password = { _password }
passwordEditEnabled = { passwordEditEnabled }
passwordNumberOfDigits = { _passwordNumberOfDigits }
setPassword = { setPassword }
setPasswordEditEnabled = { setPasswordEditEnabled } />
</>
)}
{
_showE2ee ? <>
<div className = 'separator-line' />
@@ -131,7 +143,11 @@ function mapStateToProps(state: IReduxState) {
locked,
password
} = state['features/base/conference'];
const { roomPasswordNumberOfDigits, buttonsWithNotifyClick } = state['features/base/config'];
const {
roomPasswordNumberOfDigits,
buttonsWithNotifyClick
} = state['features/base/config'];
const { disableLobbyPassword } = getSecurityUiConfig(state);
const showE2ee = Boolean(e2eeSupported) && isLocalParticipantModerator(state);
@@ -140,6 +156,7 @@ function mapStateToProps(state: IReduxState) {
_canEditPassword: isLocalParticipantModerator(state),
_conference: conference,
_dialIn: state['features/invite'],
_disableLobbyPassword: disableLobbyPassword,
_locked: locked,
_password: password,
_passwordNumberOfDigits: roomPasswordNumberOfDigits,

View File

@@ -1,14 +1,21 @@
// @flow
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { IconMic, IconVolumeUp } from '../../../../base/icons';
import { IReduxState, IStore } from '../../../../app/types';
import { translate } from '../../../../base/i18n/functions';
import { IconMic, IconVolumeUp } from '../../../../base/icons/svg';
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
import { equals } from '../../../../base/redux';
import { createLocalAudioTracks } from '../../../functions';
import { equals } from '../../../../base/redux/functions';
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 { toggleNoiseSuppression } from '../../../../noise-suppression/actions';
import { isNoiseSuppressionEnabled } from '../../../../noise-suppression/functions';
import { isPrejoinPageVisible } from '../../../../prejoin/functions';
import { createLocalAudioTracks } from '../../../functions.web';
import AudioSettingsHeader from './AudioSettingsHeader';
import MicrophoneEntry from './MicrophoneEntry';
import SpeakerEntry from './SpeakerEntry';
@@ -22,65 +29,75 @@ const browser = JitsiMeetJS.util.browser;
* @param {Function} t - The translation function.
* @returns {string}
*/
function transformDefaultDeviceLabel(deviceId, label, t) {
function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) {
return deviceId === 'default'
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
: label;
}
export type Props = {
export interface IProps extends WithTranslation {
/**
/**
* The deviceId of the microphone in use.
*/
currentMicDeviceId: string,
currentMicDeviceId: string;
/**
/**
* The deviceId of the output device in use.
*/
currentOutputDeviceId: string,
currentOutputDeviceId?: string;
/**
* Used to decide whether to measure audio levels for microphone devices.
*/
measureAudioLevels: boolean,
measureAudioLevels: boolean;
/**
* Used to set a new microphone as the current one.
*/
setAudioInputDevice: Function,
/**
* Used to set a new output device as the current one.
*/
setAudioOutputDevice: Function,
/**
* A list of objects containing the labels and deviceIds
* of all the output devices.
*/
outputDevices: Object[],
/**
/**
* A list with objects containing the labels and deviceIds
* of all the input devices.
*/
microphoneDevices: Object[],
microphoneDevices: Array<{ deviceId: string; label: string; }>;
/**
* Invoked to obtain translated strings.
* Whether noise suppression is enabled or not.
*/
t: Function
};
noiseSuppressionEnabled: boolean;
/**
* A list of objects containing the labels and deviceIds
* of all the output devices.
*/
outputDevices: Array<{ deviceId: string; label: string; }>;
/**
* Whether the prejoin page is visible or not.
*/
prejoinVisible: boolean;
/**
* Used to set a new microphone as the current one.
*/
setAudioInputDevice: Function;
/**
* Used to set a new output device as the current one.
*/
setAudioOutputDevice: Function;
/**
* Function to toggle noise suppression.
*/
toggleSuppression: () => void;
}
type State = {
/**
/**
* An list of objects, each containing the microphone label, audio track, device id
* and track error if the case.
*/
audioTracks: Object[]
}
audioTracks: Array<{ deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }>;
};
/**
* Implements a React {@link Component} which displays a list of all
@@ -88,9 +105,8 @@ type State = {
*
* @augments Component
*/
class AudioSettingsContent extends Component<Props, State> {
class AudioSettingsContent extends Component<IProps, State> {
_componentWasUnmounted: boolean;
_audioContentRef: Object;
microphoneHeaderId = 'microphone_settings_header';
speakerHeaderId = 'speaker_settings_header';
@@ -101,13 +117,11 @@ class AudioSettingsContent extends Component<Props, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: IProps) {
super(props);
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
this._onEscClick = this._onEscClick.bind(this);
this._audioContentRef = React.createRef();
this.state = {
audioTracks: props.microphoneDevices.map(({ deviceId, label }) => {
@@ -120,23 +134,6 @@ class AudioSettingsContent extends Component<Props, State> {
})
};
}
_onEscClick: (KeyboardEvent) => void;
/**
* Click handler for the speaker entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event) {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this._audioContentRef.current.style.display = 'none';
}
}
_onMicrophoneEntryClick: (string) => void;
/**
* Click handler for the microphone entries.
@@ -144,19 +141,17 @@ class AudioSettingsContent extends Component<Props, State> {
* @param {string} deviceId - The deviceId for the clicked microphone.
* @returns {void}
*/
_onMicrophoneEntryClick(deviceId) {
_onMicrophoneEntryClick(deviceId: string) {
this.props.setAudioInputDevice(deviceId);
}
_onSpeakerEntryClick: (string) => void;
/**
* Click handler for the speaker entries.
*
* @param {string} deviceId - The deviceId for the clicked speaker.
* @returns {void}
*/
_onSpeakerEntryClick(deviceId) {
_onSpeakerEntryClick(deviceId: string) {
this.props.setAudioOutputDevice(deviceId);
}
@@ -169,7 +164,8 @@ class AudioSettingsContent extends Component<Props, State> {
* @param {Function} t - The translation function.
* @returns {React$Node}
*/
_renderMicrophoneEntry(data, index, length, t) {
_renderMicrophoneEntry(data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; },
index: number, length: number, t: Function) {
const { deviceId, jitsiTrack, hasError } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const isSelected = deviceId === this.props.currentMicDeviceId;
@@ -200,7 +196,7 @@ class AudioSettingsContent extends Component<Props, State> {
* @param {Function} t - The translation function.
* @returns {React$Node}
*/
_renderSpeakerEntry(data, index, length, t) {
_renderSpeakerEntry(data: { deviceId: string; label: string; }, index: number, length: number, t: Function) {
const { deviceId } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const key = `se-${index}`;
@@ -253,9 +249,9 @@ class AudioSettingsContent extends Component<Props, State> {
* @param {Object} audioTracks - The object holding the audio tracks.
* @returns {void}
*/
_disposeTracks(audioTracks) {
_disposeTracks(audioTracks: Array<{ jitsiTrack: any; }>) {
audioTracks.forEach(({ jitsiTrack }) => {
jitsiTrack && jitsiTrack.dispose();
jitsiTrack?.dispose();
});
}
@@ -283,7 +279,7 @@ class AudioSettingsContent extends Component<Props, State> {
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: IProps) {
if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
this._setTracks();
}
@@ -296,55 +292,82 @@ class AudioSettingsContent extends Component<Props, State> {
* @inheritdoc
*/
render() {
const { outputDevices, t } = this.props;
const { outputDevices, t, noiseSuppressionEnabled, toggleSuppression, prejoinVisible } = this.props;
return (
<div>
<div
aria-labelledby = 'audio-settings-button'
className = 'audio-preview-content'
id = 'audio-settings-dialog'
onKeyDown = { this._onEscClick }
ref = { this._audioContentRef }
role = 'menu'
tabIndex = { -1 }>
<div role = 'menuitem'>
<AudioSettingsHeader
IconComponent = { IconMic }
id = { this.microphoneHeaderId }
text = { t('settings.microphones') } />
<ContextMenu
aria-labelledby = 'audio-settings-button'
className = 'audio-preview-content'
hidden = { false }
id = 'audio-settings-dialog'
tabIndex = { -1 }>
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = { t('settings.microphones') }
className = 'audio-preview-header'
icon = { IconMic }
id = { this.microphoneHeaderId }
text = { t('settings.microphones') } />
<ul
aria-labelledby = { this.microphoneHeaderId }
className = 'audio-preview-content-ul'
role = 'radiogroup'
tabIndex = { -1 }>
{this.state.audioTracks.map((data, i) =>
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
)}
</ul>
</ContextMenuItemGroup>
{ outputDevices.length > 0 && (
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = { t('settings.speakers') }
className = 'audio-preview-header'
icon = { IconVolumeUp }
id = { this.speakerHeaderId }
text = { t('settings.speakers') } />
<ul
aria-labelledby = 'microphone_settings_header'
aria-labelledby = { this.speakerHeaderId }
className = 'audio-preview-content-ul'
role = 'radiogroup'
tabIndex = '-1'>
{this.state.audioTracks.map((data, i) =>
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
tabIndex = { -1 }>
{ outputDevices.map((data, i) =>
this._renderSpeakerEntry(data, i, outputDevices.length, t)
)}
</ul>
</div>
{ outputDevices.length > 0 && (
<div role = 'menuitem'>
<hr className = 'audio-preview-hr' />
<AudioSettingsHeader
IconComponent = { IconVolumeUp }
id = { this.speakerHeaderId }
text = { t('settings.speakers') } />
<ul
aria-labelledby = 'speaker_settings_header'
className = 'audio-preview-content-ul'
role = 'radiogroup'
tabIndex = '-1'>
{ outputDevices.map((data, i) =>
this._renderSpeakerEntry(data, i, outputDevices.length, t)
)}
</ul>
</div>)
}
</div>
</div>
</ContextMenuItemGroup>)
}
{!prejoinVisible && (
<ContextMenuItemGroup>
<div
className = 'audio-preview-checkbox-container'
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { noiseSuppressionEnabled }
label = { t('toolbar.noiseSuppression') }
onChange = { toggleSuppression } />
</div>
</ContextMenuItemGroup>
)}
</ContextMenu>
);
}
}
export default translate(AudioSettingsContent);
const mapStateToProps = (state: IReduxState) => {
return {
noiseSuppressionEnabled: isNoiseSuppressionEnabled(state),
prejoinVisible: isPrejoinPageVisible(state)
};
};
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
toggleSuppression() {
dispatch(toggleNoiseSuppression());
}
};
};
export default translate(connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent));

View File

@@ -1,64 +0,0 @@
// @flow
import React from 'react';
import { Icon, IconCheck, IconExclamationSolid } from '../../../../base/icons';
/**
* The type of the React {@code Component} props of {@link AudioSettingsEntry}.
*/
export type Props = {
/**
* The text for this component.
*/
children: React$Node,
/**
* Flag indicating an error.
*/
hasError?: boolean,
/**
* The id for the label, that contains the item text.
*/
labelId?: string,
/**
* Flag indicating the selection state.
*/
isSelected: boolean,
};
/**
* React {@code Component} representing an entry for the audio settings.
*
* @returns { ReactElement}
*/
export default function AudioSettingsEntry(
{ children, hasError, labelId, isSelected }: Props) {
const className = `audio-preview-entry ${isSelected
? 'audio-preview-entry--selected' : ''}`;
return (
<div className = { className }>
{isSelected && (
<Icon
className = 'audio-preview-icon audio-preview-icon--check'
color = '#1C2025'
size = { 14 }
src = { IconCheck } />
)}
<span
className = 'audio-preview-entry-text'
id = { labelId }>
{children}
</span>
{hasError && <Icon
className = 'audio-preview-icon audio-preview-icon--exclamation'
size = { 16 }
src = { IconExclamationSolid } />}
</div>
);
}

View File

@@ -1,48 +0,0 @@
// @flow
import React from 'react';
import { Icon } from '../../../../base/icons';
/**
* The type of the React {@code Component} props of {@link AudioSettingsHeader}.
*/
type Props = {
/**
* The id used for the Header-text.
*/
id?: string,
/**
* The Icon used for the Header.
*/
IconComponent: Function,
/**
* The text of the Header.
*/
text: string,
};
/**
* React {@code Component} representing the Header of an audio option group.
*
* @returns { ReactElement}
*/
export default function AudioSettingsHeader({ IconComponent, id, text }: Props) {
return (
<div
className = 'audio-preview-header'
role = 'heading'>
<div className = 'audio-preview-header-icon'>
{ <Icon
size = { 20 }
src = { IconComponent } />}
</div>
<div
className = 'audio-preview-header-text'
id = { id } >{text}</div>
</div>
);
}

View File

@@ -1,8 +1,8 @@
// @flow
import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
import React from 'react';
import { areAudioLevelsEnabled } from '../../../../base/config/functions';
import { IReduxState } from '../../../../app/types';
import { areAudioLevelsEnabled } from '../../../../base/config/functions.web';
import {
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice as setAudioOutputDeviceAction
@@ -12,39 +12,38 @@ import {
getAudioOutputDeviceData
} from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
import {
getCurrentMicDeviceId,
getCurrentOutputDeviceId
} from '../../../../base/settings';
} from '../../../../base/settings/functions.web';
import { toggleAudioSettings } from '../../../actions';
import { getAudioSettingsVisibility } from '../../../functions';
import { getAudioSettingsVisibility } from '../../../functions.web';
import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent';
import AudioSettingsContent, { type IProps as AudioSettingsContentProps } from './AudioSettingsContent';
type Props = AudioSettingsContentProps & {
interface IProps extends AudioSettingsContentProps {
/**
* Component's children (the audio button).
*/
children: React$Node,
children: ReactNode;
/**
* Flag controlling the visibility of the popup.
*/
isOpen: boolean,
isOpen: boolean;
/**
* Callback executed when the popup closes.
*/
onClose: Function,
onClose: Function;
/**
* The popup placement enum value.
*/
popupPlacement: string
popupPlacement: string;
}
/**
@@ -64,7 +63,7 @@ function AudioSettingsPopup({
outputDevices,
popupPlacement,
measureAudioLevels
}: Props) {
}: IProps) {
return (
<div className = 'audio-preview'>
<Popover
@@ -92,16 +91,16 @@ function AudioSettingsPopup({
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
function mapStateToProps(state: IReduxState) {
const { clientWidth } = state['features/base/responsive-ui'];
return {
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
currentMicDeviceId: getCurrentMicDeviceId(state),
currentOutputDeviceId: getCurrentOutputDeviceId(state),
isOpen: getAudioSettingsVisibility(state),
microphoneDevices: getAudioInputDeviceData(state),
outputDevices: getAudioOutputDeviceData(state),
isOpen: Boolean(getAudioSettingsVisibility(state)),
microphoneDevices: getAudioInputDeviceData(state) ?? [],
outputDevices: getAudioOutputDeviceData(state) ?? [],
measureAudioLevels: areAudioLevelsEnabled(state)
};
}

View File

@@ -1,34 +1,33 @@
// @flow
import React from 'react';
import { Icon, IconMeter } from '../../../../base/icons';
import Icon from '../../../../base/icons/components/Icon';
import { IconMeter } from '../../../../base/icons/svg';
type Props = {
interface IProps {
/**
* Own class name for the component.
*/
className: string,
className: string;
/**
* Flag indicating whether the component is greyed out/disabled.
*/
isDisabled?: boolean,
isDisabled?: boolean;
/**
* The level of the meter.
* Should be between 0 and 7 as per the used SVG.
*/
level: number,
};
level: number;
}
/**
* React {@code Component} representing an audio level meter.
*
* @returns { ReactElement}
*/
export default function({ className, isDisabled, level }: Props) {
export default function({ className, isDisabled, level }: IProps) {
let ownClassName;
if (level > -1) {

View File

@@ -1,61 +1,80 @@
// @flow
import clsx from 'clsx';
import React, { Component } from 'react';
import Icon from '../../../../base/icons/components/Icon';
import { IconCheck, IconExclamationSolid } from '../../../../base/icons/svg';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_';
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
import AudioSettingsEntry, { type Props as AudioSettingsEntryProps } from './AudioSettingsEntry';
import Meter from './Meter';
const JitsiTrackEvents = JitsiMeetJS.events.track;
type Props = AudioSettingsEntryProps & {
type Props = {
/**
* The text for this component.
*/
children: string;
/**
* The deviceId of the microphone.
*/
deviceId: string,
deviceId: string;
/**
* Flag indicating if there is a problem with the device.
*/
hasError?: boolean,
hasError?: boolean;
/**
* Flag indicating if there is a problem with the device.
*/
index?: number,
index?: number;
/**
* Flag indicating the selection state.
*/
isSelected: boolean;
/**
* The audio track for the current entry.
*/
jitsiTrack: Object,
jitsiTrack: any;
/**
* The id for the label, that contains the item text.
*/
labelId?: string;
/**
* The length of the microphone list.
*/
length: number,
length: number;
/**
* Click handler for component.
*/
onClick: Function,
listHeaderId: string,
listHeaderId: string;
/**
* Used to decide whether to listen to audio level changes.
*/
measureAudioLevels: boolean,
}
measureAudioLevels: boolean;
/**
* Click handler for component.
*/
onClick: Function;
};
type State = {
/**
* The audio level.
*/
level: number
}
level: number;
};
/**
* React {@code Component} representing an entry for the microphone audio settings.
@@ -81,8 +100,6 @@ export default class MicrophoneEntry extends Component<Props, State> {
this._updateLevel = this._updateLevel.bind(this);
}
_onClick: () => void;
/**
* Click handler for the entry.
*
@@ -92,13 +109,6 @@ export default class MicrophoneEntry extends Component<Props, State> {
this.props.onClick(this.props.deviceId);
}
/**
* Key pressed handler for the entry.
*
* @returns {void}
*/
_onKeyPress: (KeyboardEvent) => void;
/**
* Key pressed handler for the entry.
*
@@ -107,22 +117,20 @@ export default class MicrophoneEntry extends Component<Props, State> {
*
* @returns {void}
*/
_onKeyPress(e) {
_onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ') {
e.preventDefault();
this.props.onClick(this.props.deviceId);
}
}
_updateLevel: (number) => void;
/**
* Updates the level of the meter.
*
* @param {number} num - The audio level provided by the jitsiTrack.
* @returns {void}
*/
_updateLevel(num) {
_updateLevel(num: number) {
this.setState({
level: Math.floor(num / 0.125)
});
@@ -147,8 +155,8 @@ export default class MicrophoneEntry extends Component<Props, State> {
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
* @returns {void}
*/
_stopListening(jitsiTrack) {
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
_stopListening(jitsiTrack?: any) {
jitsiTrack?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
this.setState({
level: -1
});
@@ -202,9 +210,9 @@ export default class MicrophoneEntry extends Component<Props, State> {
measureAudioLevels
} = this.props;
const deviceTextId: string = `choose_microphone${deviceId}`;
const deviceTextId = `choose_microphone${deviceId}`;
const labelledby: string = `${listHeaderId} ${deviceTextId} `;
const labelledby = `${listHeaderId} ${deviceTextId} `;
const className = `audio-preview-microphone ${measureAudioLevels
? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`;
@@ -220,12 +228,17 @@ export default class MicrophoneEntry extends Component<Props, State> {
onKeyPress = { this._onKeyPress }
role = 'radio'
tabIndex = { 0 }>
<AudioSettingsEntry
hasError = { hasError }
isSelected = { isSelected }
labelId = { deviceTextId }>
{children}
</AudioSettingsEntry>
<ContextMenuItem
accessibilityLabel = ''
icon = { isSelected ? IconCheck : undefined }
selected = { isSelected }
text = { children }
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
{hasError && <Icon
className = 'audio-preview-icon audio-preview-icon--exclamation'
size = { 16 }
src = { IconExclamationSolid } />}
</ContextMenuItem>
{ Boolean(jitsiTrack) && measureAudioLevels && <Meter
className = 'audio-preview-meter-mic'
isDisabled = { hasError }

View File

@@ -1,163 +0,0 @@
// @flow
import React, { Component } from 'react';
import logger from '../../../logger';
import AudioSettingsEntry from './AudioSettingsEntry';
import TestButton from './TestButton';
const TEST_SOUND_PATH = 'sounds/ring.mp3';
/**
* The type of the React {@code Component} props of {@link SpeakerEntry}.
*/
type Props = {
/**
* The text label for the entry.
*/
children: React$Node,
/**
* Flag controlling the selection state of the entry.
*/
isSelected: boolean,
/**
* Flag controlling the selection state of the entry.
*/
index: number,
/**
* Flag controlling the selection state of the entry.
*/
length: number,
/**
* The deviceId of the speaker.
*/
deviceId: string,
/**
* Click handler for the component.
*/
onClick: Function,
listHeaderId: string
};
/**
* Implements a React {@link Component} which displays an audio
* output settings entry. The user can click and play a test sound.
*
* @augments Component
*/
export default class SpeakerEntry extends Component<Props> {
/**
* A React ref to the HTML element containing the {@code audio} instance.
*/
audioRef: Object;
/**
* Initializes a new {@code SpeakerEntry} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.audioRef = React.createRef();
this._onTestButtonClick = this._onTestButtonClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
}
_onClick: () => void;
/**
* Click handler for the entry.
*
* @returns {void}
*/
_onClick() {
this.props.onClick(this.props.deviceId);
}
_onKeyPress: () => void;
/**
* Key pressed handler for the entry.
*
* @param {Object} e - The event.
* @private
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ') {
e.preventDefault();
this.props.onClick(this.props.deviceId);
}
}
_onTestButtonClick: Object => void;
/**
* Click handler for Test button.
* Sets the current audio output id and plays a sound.
*
* @param {Object} e - The sythetic event.
* @returns {void}
*/
async _onTestButtonClick(e) {
e.stopPropagation();
try {
await this.audioRef.current.setSinkId(this.props.deviceId);
this.audioRef.current.play();
} catch (err) {
logger.log('Could not set sink id', err);
}
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { children, isSelected, index, deviceId, length, listHeaderId } = this.props;
const deviceTextId: string = `choose_speaker${deviceId}`;
const labelledby: string = `${listHeaderId} ${deviceTextId} `;
return (
<li
aria-checked = { isSelected }
aria-labelledby = { labelledby }
aria-posinset = { index }
aria-setsize = { length }
className = 'audio-preview-speaker'
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'radio'
tabIndex = { 0 }>
<AudioSettingsEntry
isSelected = { isSelected }
key = { deviceId }
labelId = { deviceTextId }>
{children}
</AudioSettingsEntry>
<TestButton
onClick = { this._onTestButtonClick }
onKeyPress = { this._onTestButtonClick } />
<audio
preload = 'auto'
ref = { this.audioRef }
src = { TEST_SOUND_PATH } />
</li>
);
}
}

View File

@@ -0,0 +1,140 @@
import clsx from 'clsx';
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 logger from '../../../logger';
const TEST_SOUND_PATH = 'sounds/ring.mp3';
/**
* The type of the React {@code Component} props of {@link SpeakerEntry}.
*/
interface IProps {
/**
* The text label for the entry.
*/
children: string;
/**
* The deviceId of the speaker.
*/
deviceId: string;
/**
* Flag controlling the selection state of the entry.
*/
index: number;
/**
* Flag controlling the selection state of the entry.
*/
isSelected: boolean;
/**
* Flag controlling the selection state of the entry.
*/
length: number;
listHeaderId: string;
/**
* Click handler for the component.
*/
onClick: Function;
}
/**
* Implements a React {@link Component} which displays an audio
* output settings entry. The user can click and play a test sound.
*
* @param {IProps} props - Component props.
* @returns {JSX.Element}
*/
const SpeakerEntry = (props: IProps) => {
const audioRef = useRef<HTMLAudioElement | null>(null);
/**
* Click handler for the entry.
*
* @returns {void}
*/
function _onClick() {
props.onClick(props.deviceId);
}
/**
* Key pressed handler for the entry.
*
* @param {Object} e - The event.
* @private
*
* @returns {void}
*/
function _onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ') {
e.preventDefault();
props.onClick(props.deviceId);
}
}
/**
* Click handler for Test button.
* Sets the current audio output id and plays a sound.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
async function _onTestButtonClick(e: React.KeyboardEvent | React.MouseEvent) {
e.stopPropagation();
try { // @ts-ignore
await audioRef.current?.setSinkId(props.deviceId);
audioRef.current?.play();
} catch (err) {
logger.log('Could not set sink id', err);
}
}
const { children, isSelected, index, deviceId, length, listHeaderId } = props;
const deviceTextId = `choose_speaker${deviceId}`;
const labelledby = `${listHeaderId} ${deviceTextId} `;
/* eslint-disable react/jsx-no-bind */
return (
<li
aria-checked = { isSelected }
aria-labelledby = { labelledby }
aria-posinset = { index }
aria-setsize = { length }
className = 'audio-preview-speaker'
onClick = { _onClick }
onKeyPress = { _onKeyPress }
role = 'radio'
tabIndex = { 0 }>
<ContextMenuItem
accessibilityLabel = ''
icon = { isSelected ? IconCheck : undefined }
selected = { isSelected }
text = { children }
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
<Button
className = 'audio-preview-test-button'
label = 'Test'
onClick = { _onTestButtonClick }
onKeyPress = { _onTestButtonClick }
type = { BUTTON_TYPES.SECONDARY } />
</ContextMenuItem>
<audio
preload = 'auto'
ref = { audioRef }
src = { TEST_SOUND_PATH } />
</li>
);
};
export default SpeakerEntry;

View File

@@ -1,34 +0,0 @@
// @flow
import React from 'react';
type Props = {
/**
* Click handler for the button.
*/
onClick: Function,
/**
* Keypress handler for the button.
*/
onKeyPress: Function,
};
/**
* React {@code Component} representing an button used for testing output sound.
*
* @returns { ReactElement}
*/
export default function TestButton({ onClick, onKeyPress }: Props) {
return (
<div
className = 'audio-preview-test-button'
onClick = { onClick }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
Test
</div>
);
}

View File

@@ -42,7 +42,7 @@ export function createLocalVideoTracks(ids: string[], timeout?: number) {
* label: string
* }[]>}
*/
export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) {
export function createLocalAudioTracks(devices: Array<{ deviceId: string; label: string; }>, timeout?: number) {
return Promise.all(
devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null;

View File

@@ -0,0 +1,44 @@
import React from 'react';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
type Props = AbstractButtonProps & {
icon: string;
text: string;
};
/**
* Component that renders a custom toolbox button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<Props, any, any> {
// @ts-ignore
iconSrc = this.props.icon;
// @ts-ignore
id = this.props.id;
// @ts-ignore
text = this.props.text;
accessibilityLabel = this.text;
/**
* Custom icon component.
*
* @param {any} props - Icon's props.
* @returns {img}
*/
icon = (props: any) => (<img
src = { this.iconSrc }
{ ...props } />);
label = this.text;
tooltip = this.text;
}
export default CustomOptionButton;

View File

@@ -1,5 +1,8 @@
import { ReactNode, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
type Props = {
@@ -36,6 +39,7 @@ type Props = {
* @returns {ReactElement}
*/
function DialogPortal({ children, className, style, getRef, setSize }: Props) {
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
@@ -92,7 +96,7 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
document.body.removeChild(portalTarget);
}
};
}, []);
}, [ clientWidth ]);
return ReactDOM.createPortal(
children,

View File

@@ -12,6 +12,7 @@ import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { IJitsiConference } from '../../../base/conference/reducer';
import {
getButtonsWithNotifyClick,
getMultipleVideoSendingSupportFeatureFlag,
getToolbarButtons,
isToolbarButtonEnabled
@@ -120,6 +121,7 @@ import HelpButton from '../HelpButton';
// @ts-ignore
import AudioSettingsButton from './AudioSettingsButton';
import CustomOptionButton from './CustomOptionButton';
import { EndConferenceButton } from './EndConferenceButton';
// @ts-ignore
import FullscreenButton from './FullscreenButton';
@@ -174,6 +176,11 @@ interface IProps extends WithTranslation {
*/
_conference?: IJitsiConference;
/**
* Custom Toolbar buttons.
*/
_customToolbarButtons?: Array<{ icon: string; id: string; text: string; }>;
/**
* Whether or not screensharing button is disabled.
*/
@@ -358,7 +365,8 @@ const styles = () => {
right: 'auto',
margin: 0,
marginBottom: '8px',
maxHeight: 'calc(100vh - 100px)'
maxHeight: 'calc(100vh - 100px)',
width: '240px'
},
hangupMenu: {
@@ -714,6 +722,7 @@ class Toolbox extends Component<IProps> {
*/
_getAllButtons() {
const {
_customToolbarButtons,
_feedbackConfigured,
_hasSalesforce,
_isIosMobile,
@@ -914,6 +923,19 @@ class Toolbox extends Component<IProps> {
group: 4
};
const customButtons = _customToolbarButtons?.reduce((prev, { icon, id, text }) => {
return {
...prev,
[id]: {
key: id,
Content: CustomOptionButton,
group: 4,
icon,
text
}
};
}, {});
return {
microphone,
camera,
@@ -944,7 +966,8 @@ class Toolbox extends Component<IProps> {
embed,
feedback,
download,
help
help,
...customButtons
};
}
@@ -1524,8 +1547,8 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const endConferenceSupported = conference?.isEndConferenceSupported() && isLocalParticipantModerator(state);
const {
buttonsWithNotifyClick,
callStatsID,
customToolbarButtons,
disableProfile,
iAmRecorder,
iAmSipGateway
@@ -1543,10 +1566,11 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
return {
_backgroundType: state['features/virtual-background'].backgroundType ?? '',
_buttonsWithNotifyClick: buttonsWithNotifyClick,
_buttonsWithNotifyClick: getButtonsWithNotifyClick(state),
_chatOpen: state['features/chat'].isOpen,
_clientWidth: clientWidth,
_conference: conference,
_customToolbarButtons: customToolbarButtons,
_desktopSharingEnabled: JitsiMeetJS.isDesktopSharingEnabled(),
_desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
_dialog: Boolean(state['features/base/dialog'].component),

View File

@@ -1,7 +1,7 @@
// @flow
import { openDialog } from '../../base/dialog';
import { IconCrown } from '../../base/icons';
import { IconModerator } from '../../base/icons';
import {
PARTICIPANT_ROLE,
getLocalParticipant,
@@ -35,7 +35,7 @@ export type Props = AbstractButtonProps & {
*/
export default class AbstractGrantModeratorButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
icon = IconCrown;
icon = IconModerator;
label = 'videothumbnail.grantModerator';
/**

View File

@@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
const CustomOptionButton = (
{ icon: iconSrc, onClick, text }:
{
icon: string;
onClick: (e?: React.MouseEvent<Element, MouseEvent> | undefined) => void;
text: string;
}
) => {
const icon = useCallback(props => (<img
src = { iconSrc }
{ ...props } />), [ iconSrc ]);
return (
<ContextMenuItem
accessibilityLabel = { text }
icon = { icon }
onClick = { onClick }
text = { text } />
);
};
export default CustomOptionButton;

View File

@@ -3,7 +3,7 @@
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconCrown } from '../../../base/icons';
import { IconModerator } from '../../../base/icons';
import { connect } from '../../../base/redux';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import AbstractGrantModeratorButton, {
@@ -46,7 +46,7 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
className = 'grantmoderatorlink'
icon = { IconCrown }
icon = { IconModerator }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('videothumbnail.grantModerator') } />

View File

@@ -26,6 +26,7 @@ import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import CustomOptionButton from './CustomOptionButton';
// @ts-ignore
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
// @ts-ignore
@@ -144,7 +145,7 @@ const ParticipantContextMenu = ({
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
const { remoteVideoMenu = {}, disableRemoteMute, startSilent }
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
= useSelector((state: IReduxState) => state['features/base/config']);
const { disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
@@ -171,8 +172,8 @@ const ParticipantContextMenu = ({
}
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
const buttons = [];
const buttons2 = [];
const buttons: JSX.Element[] = [];
const buttons2: JSX.Element[] = [];
const showVolumeSlider = !startSilent
&& !isIosMobileBrowser()
@@ -277,6 +278,23 @@ const ParticipantContextMenu = ({
);
}
if (customParticipantMenuButtons) {
customParticipantMenuButtons.forEach(
({ icon, id, text }) => {
const onClick = useCallback(
() => APP.API.notifyParticipantMenuButtonClicked(id, _getCurrentParticipantId()), []);
buttons2.push(
<CustomOptionButton
icon = { icon }
key = { id }
onClick = { onClick }
text = { text } />
);
}
);
}
const breakoutRoomsButtons: any = [];
if (!thumbnailMenu && _isModerator) {

View File

@@ -54,23 +54,22 @@ const styles = (theme: Theme) => {
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
padding: '0 5px',
padding: '10px 16px',
'&:hover': {
backgroundColor: theme.palette.ui04
backgroundColor: theme.palette.ui02
}
},
icon: {
minWidth: '20px',
padding: '5px',
marginRight: '16px',
position: 'relative' as const
},
sliderContainer: {
position: 'relative' as const,
width: '100%',
paddingRight: '5px'
width: '100%'
},
slider: {

View File

@@ -2,9 +2,9 @@
"include": ["react/features/**/*.ts", "react/features/**/*.tsx", "./custom.d.ts", "./globals.native.d.ts"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "ESNext",
"target": "es2020",
"jsx": "react-native",
"lib": [ "ES2017"],
"lib": [ "es2020"],
"types": [ "react-native" ],
"skipLibCheck": true,
"moduleResolution": "Node",

View File

@@ -2,8 +2,8 @@
"include": ["react/features/**/*.ts", "react/features/**/*.tsx", "./custom.d.ts", "./globals.d.ts"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"module": "es6",
"target": "es6",
"module": "es2020",
"target": "es2020",
"jsx": "react",
"lib": [ "webworker", "ES2020", "DOM" ],
"skipLibCheck": true,