Compare commits

...

21 Commits

Author SHA1 Message Date
Aaron van Meerten
de81d395c1 feature: allowlist checking for token verification module 2023-06-23 15:44:41 -04:00
damencho
9af56d52c2 fix: Adds one more check for iframes. 2023-06-23 11:05:37 -05:00
José Luís Andrade
2a9c40f0d2 lang: Update Portuguese translation (#13481) 2023-06-23 09:18:46 -05:00
Jaya Allamsetty
3ae18be21f fix(lastn) Update lastN on virtual screenshare updates.
Fixes https://github.com/jitsi/jitsi-meet/issues/13448.
2023-06-21 12:30:16 -04:00
Calinteodor
9f5dbb21a7 feat(base/media): fixed movement inside zoomed screenshare (#13476)
* feat(base/media): fixed movement inside zoomed screenshare
2023-06-21 11:59:03 +03:00
Mihaela Dumitru
2d14990b9e chore(deps) update excalidraw to fix load issues with bigger whiteboards (#13474) 2023-06-20 17:43:22 +03:00
Calin-Teodor
169c8ecb62 sdk(react-native-sdk): added generated folders to gitignore 2023-06-20 16:23:19 +03:00
Horatiu Muresan
d608cf40f5 fix(prejoin) Check for valid url for prejoin (#13468)
- `getPropertyValue` calls `parseUrlParam` with the connection URL from store, which is not yet defined
2023-06-19 15:52:38 +03:00
Emmanuel Pelletier
51a4e7daa3 Globally improve accessibility for screen reader users (#12969)
feat(a11y): Globally improve accessibility for screen reader users
2023-06-19 14:34:41 +03:00
arunnadesh
7538bfc713 fix(AudioTrack) fix currentMuted
Co-authored-by: Arun Nadesh <arun.raveendran@hg.ninjavan.co>
2023-06-19 09:51:46 +02:00
Saúl Ibarra Corretgé
48e1f443ea fix(password) use the numeric input mode when only digits are required
Fixes: https://github.com/jitsi/brave-tracker/issues/101
2023-06-16 15:50:27 +02:00
Robert Pintilii
2292ebe762 fix(transcriptions) Open correct settings tab (#13460) 2023-06-15 16:02:12 +03:00
Hristo Terezov
5425b52615 fix(horizontal-filmstrip): JS error.
Fixes the following JS error which prevents the whole page from
rendering:
TypeError: Cannot read properties of null (reading 'offsetHeight')
2023-06-14 18:28:32 -05:00
Hristo Terezov
74f605e045 fix(screenLock): Improve.
- Add debug logs.
 - Re-request wake lock if it is released by the OS because of page
visibility.
2023-06-14 11:15:37 -05:00
Alexander Bigga
1918566581 fix(lang) update German translation 2023-06-13 11:21:36 -05:00
Saúl Ibarra Corretgé
ee8ba6696d fix(full-screen) drop no longer needed checks
The vendored prefix on Firefox was removed on version 64.

We still need the vendored version for Safari since the prefix got
dropped in 16.4.
2023-06-12 13:55:45 +02:00
Saúl Ibarra Corretgé
15df3cb11e fix(toolbox) drop unneeded checks
These are web files, no need to check if APP is undefined.
2023-06-12 13:55:45 +02:00
Robert Pintilii
b77db024f5 fix(settings-dialog) On mobile open on the correct tab (#13443) 2023-06-12 13:55:32 +03:00
Robert Pintilii
c8a87e368a fix(local-rec) Fix audio only recording (self) (#13442) 2023-06-12 11:21:20 +03:00
garysmith058
277ca23c52 feat(external-api) Forward non participant message to iframe (#13440)
* Forward non-participant-message-received to iFrame API

* Updated comment

* Fix lint errors
2023-06-12 11:10:42 +03:00
Eze Posada
55f66e236e fix(lang) update Spanish translation 2023-06-11 08:53:48 +02:00
86 changed files with 1342 additions and 528 deletions

12
.gitignore vendored
View File

@@ -94,3 +94,15 @@ twa/*.aab
twa/assetlinks.json
tsconfig.json
# React Native SDK
#
react-native-sdk/android/src
react-native-sdk/images
react-native-sdk/ios
react-native-sdk/lang
react-native-sdk/modules
react-native-sdk/node_modules
react-native-sdk/react
react-native-sdk/service
react-native-sdk/sounds

View File

@@ -2029,7 +2029,10 @@ export default {
room.on(
JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
(...args) => APP.store.dispatch(nonParticipantMessageReceived(...args)));
(...args) => {
APP.store.dispatch(nonParticipantMessageReceived(...args));
APP.API.notifyNonParticipantMessageReceived(...args);
});
room.on(
JitsiConferenceEvents.LOCK_STATE_CHANGED,

View File

@@ -41,3 +41,36 @@
display: -webkit-flex !important;
display: flex !important;
}
/**
* resets default button styles,
* mostly intended to be used on interactive elements that
* differ from their default styles (e.g. <a>) or have custom styles
*/
.invisible-button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}
/**
* style an element the same as an <a>
* useful on some cases where we visually have a link but it's actually a <button>
*/
.as-link {
@extend .invisible-button;
display: inline;
color: #44A5FF;
text-decoration: none;
font-weight: bold;
&:focus,
&:hover,
&:active {
text-decoration: underline;
}
}

View File

@@ -21,7 +21,7 @@
&-actions {
margin-top: 10px;
a {
button {
cursor: pointer;
text-decoration: none;
font-size: 14px;

View File

@@ -39,6 +39,18 @@
"audioOnly": {
"audioOnly": "Geringe Bandbreite"
},
"bandwidthSettings": {
"assumedBandwidthBps": "z.B. 10000000 für 10 Mbps",
"assumedBandwidthBpsWarning": "Höhere Werte können zu Netzwerk-Problemen führen.",
"customValue": "spezifischer Wert",
"customValueEffect": "setzt den Wert in bps",
"leaveEmpty": "leer lassen",
"leaveEmptyEffect": "aktiviert die automatische Abschätzung",
"possibleValues": "Mögliche Werte",
"setAssumedBandwidthBps": "Angenommene Bandbreite (bps)",
"title": "Einstellungen Bandbreite",
"zeroEffect": "schaltet Video aus"
},
"breakoutRooms": {
"actions": {
"add": "Breakout-Raum hinzufügen",
@@ -156,6 +168,7 @@
"localport_plural": "Lokale Ports:",
"maxEnabledResolution": "max. senden",
"more": "Mehr anzeigen",
"no": "Nein",
"packetloss": "Paketverlust:",
"participant_id": "Personen-ID:",
"quality": {
@@ -174,7 +187,8 @@
"status": "Verbindung:",
"transport": "Protokoll:",
"transport_plural": "Protokolle:",
"video_ssrc": "Video-SSRC:"
"video_ssrc": "Video-SSRC:",
"yes": "Ja"
},
"dateUtils": {
"earlier": "Früher",
@@ -673,6 +687,7 @@
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
"dataChannelClosed": "Schlechte Videoqualität",
"dataChannelClosedDescription": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher ist die Videoqulität auf die schlechteste Stufe limitiert.",
"disabledIframe": "Die Einbettung ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disconnected": "getrennt",
"displayNotifications": "Benachrichtigungen anzeigen für",
"dontRemindMe": "Nicht erinnern",
@@ -867,9 +882,11 @@
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
"or": "oder",
"premeeting": "Vorschau",
"proceedAnyway": "Trotzdem fortsetzen",
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
"showScreen": "Konferenzvorschau aktivieren",
"startWithPhone": "Mit Telefonaudio starten",
"unsafeRoomConsent": "Ich verstehe das Risiko und möchte der Konferenz beitreten",
"videoOnlyError": "Videofehler:",
"videoTrackError": "Videotrack konnte nicht erstellt werden.",
"viewAllNumbers": "alle Nummern anzeigen"
@@ -971,8 +988,14 @@
"security": {
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
"title": "Sicherheitsoptionen"
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
"title": "Sicherheitsoptionen",
"unsafeRoomActions": {
"meeting": "Erwägen Sie die Absicherung Ihrer Konferenz über den Sicherheits-Button.",
"prejoin": "Erwägen Sie einen einzigartigeren Raumnamen zu wählen.",
"welcome": "Erwägen Sie einen einzigartigeren Raumnamen zu wählen oder wählen Sie einen der Vorschläge."
}
},
"settings": {
"audio": "Audio",
@@ -1140,6 +1163,7 @@
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryoneElsesVideoStream": "Alle anderen Kameras ausschalten",
"muteEveryonesVideoStream": "Alle Kameras ausschalten",
"muteGUMPending": "Verbinde Ihr Mikrofon",
"noiseSuppression": "Rauschunterdrückung",
"openChat": "Chat öffnen",
"participants": "Anwesende",
@@ -1147,6 +1171,7 @@
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "Hand heben",
"reactions": "Interaktionen",
"reactionsMenu": "Interaktionsmenü öffnen / schließen",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten",
@@ -1172,6 +1197,7 @@
"unmute": "Stummschaltung aufheben",
"videoblur": "Unscharfer Hintergrund ein-/ausschalten",
"videomute": "„Video stummschalten“ ein-/ausschalten",
"videomuteGUMPending": "Verbinde Ihre Kamera",
"videounmute": "Kamera einschalten"
},
"addPeople": "Personen zur Konferenz hinzufügen",
@@ -1222,6 +1248,7 @@
"mute": "Stummschalten",
"muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteGUMPending": "Verbinde Ihre Kamera",
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
"noAudioSignalDialInDesc": "Sie können sich auch über die Einwahlnummer einwählen:",
@@ -1244,6 +1271,7 @@
"reactionLike": "Daumen hoch senden",
"reactionSilence": "Stille senden",
"reactionSurprised": "Überrascht senden",
"reactions": "Interaktionen",
"security": "Sicherheitsoptionen",
"selectBackground": "Hintergrund auswählen",
"shareRoom": "Person einladen",
@@ -1266,6 +1294,7 @@
"unmute": "Stummschaltung aufheben",
"videoSettings": "Kameraeinstellungen",
"videomute": "Kamera stoppen",
"videomuteGUMPending": "Verbinde Ihre Kamera",
"videounmute": "Kamera einschalten"
},
"transcribing": {
@@ -1377,7 +1406,14 @@
"webAssemblyWarning": "WebAssembly wird nicht unterstützt",
"webAssemblyWarningDescription": "WebAssembly ist deaktiviert oder wird in diesem Browser nicht unterstützt"
},
"visitorsLabel": "Anzahl Gäste: {{count}}",
"visitors": {
"chatIndicator": "(Gast)",
"labelTooltip": "Anzahl Gäste: {{count}}",
"notification": {
"description": "Bitte melden Sie sich um teilzunehmen",
"title": "Sie sind Gast in der Konferenz"
}
},
"volumeSlider": "Lautstärkeregler",
"welcomepage": {
"accessibilityLabel": {

View File

@@ -11,7 +11,6 @@
"defaultEmail": "Dirección de correo por defecto",
"disabled": "No puede invitar a otras personas.",
"failedToAdd": "Error al agregar participantes",
"footerText": "La marcación está desactivada.",
"googleEmail": "Correo electrónico de Google",
"inviteMoreHeader": "Usted se encuentra solo en la reunión",
"inviteMoreMailSubject": "Unirse a la reunión {{appName}}",
@@ -31,6 +30,7 @@
},
"audioDevices": {
"bluetooth": "Bluetooth",
"car": "Audio de automóvil",
"headphones": "Auriculares",
"none": "No hay dispositivos de audio disponibles",
"phone": "Teléfono",
@@ -39,6 +39,37 @@
"audioOnly": {
"audioOnly": "Solo sonido y pantalla compartida"
},
"bandwidthSettings": {
"assumedBandwidthBps": "por ejemplo 10000000 para 10 Mbps",
"assumedBandwidthBpsWarning": "Valores más altos podrían causar problemas de red.",
"customValue": "valor personalizado",
"customValueEffect": "para establecer el valor real de bps",
"leaveEmpty": "dejar vacío",
"leaveEmptyEffect": "para permitir que se realicen estimaciones",
"possibleValues": "Valores posibles",
"setAssumedBandwidthBps": "Ancho de banda asumido (bps)",
"title": "Ajustes de ancho de banda",
"zeroEffect": "para deshabilitar el video"
},
"breakoutRooms": {
"actions": {
"add": "Agregar sala para grupos pequeños",
"autoAssign": "Autoasignar a sala para grupos pequeños",
"close": "Cerrar",
"join": "Unirse",
"leaveBreakoutRoom": "Abandonar sala para grupos pequeños",
"more": "Más",
"remove": "Quitar",
"sendToBreakoutRoom": "Enviar participante a:"
},
"defaultName": "Sala para grupos pequeños #{{index}}",
"mainRoom": "Sala principal",
"notifications": {
"joined": "Uniéndose a la sala para grupos pequeños \"{{name}}\"",
"joinedMainRoom": "Uniéndose a la sala principal",
"joinedTitle": "Salas para grupos pequeños"
}
},
"calendarSync": {
"addMeetingURL": "Agregar un vínculo a la reunión",
"confirmAddLink": "¿Quiere añadir un enlace de Jitsi a este evento?",
@@ -57,15 +88,27 @@
"refresh": "Actualizar calendario",
"today": "Hoy"
},
"carmode": {
"actions": {
"selectSoundDevice": "Elija un dispositivo de sonido"
},
"labels": {
"buttonLabel": "Modo automóvil",
"title": "Modo automóvil",
"videoStopped": "Su video se ha detenido"
}
},
"chat": {
"enter": "Entrar en la sala",
"error": "Error: su mensaje no se envío. Motivo: {{error}}",
"fieldPlaceHolder": "Escriba su mensaje aquí",
"lobbyChatMessageTo": "Mensaje de chat de lobby a {{recipient}}",
"message": "Mensaje",
"messageAccessibleTitle": "{{user}} dice:",
"messageAccessibleTitleMe": "yo digo:",
"messageTo": "Mensaje privado para {{recipient}}",
"messagebox": "Escriba un mensaje",
"newMessages": "Mensajes nuevos",
"nickname": {
"popover": "Selecciona un apodo",
"title": "Introduce un apodo para usar el chat",
@@ -85,6 +128,7 @@
},
"chromeExtensionBanner": {
"buttonText": "Instalar extensión de Chrome",
"buttonTextEdge": "Instalar extensión de Edge",
"close": "Cerrar",
"dontShowAgain": "No mostrar nuevamente",
"installExtensionText": "Instalar la extensión para Google Calendar y la integración con Office 365"
@@ -115,6 +159,7 @@
"bridgeCount": "Contador del servidor: ",
"codecs": "Codecs (A/V):",
"connectedTo": "Conectado a:",
"e2eeVerified": "",
"framerate": "Fotogramas por segundo:",
"less": "Mostrar menos",
"localaddress": "Dirección local:",
@@ -123,6 +168,7 @@
"localport_plural": "Puertos locales:",
"maxEnabledResolution": "enviar max",
"more": "Mostrar más",
"no": "no",
"packetloss": "Pérdida de paquetes:",
"participant_id": "ID participante:",
"quality": {
@@ -141,7 +187,8 @@
"status": "Calidad:",
"transport": "Transporte:",
"transport_plural": "Transportes:",
"video_ssrc": "Video SSRC:"
"video_ssrc": "Video SSRC:",
"yes": "sí"
},
"dateUtils": {
"earlier": "Anterior",
@@ -150,15 +197,24 @@
},
"deepLinking": {
"appNotInstalled": "Necesitas la aplicación {{app}} para unirte a esta reunión en el teléfono.",
"description": "¿No pasó nada? Hemos intentado iniciar la reunión en la aplicación de escritorio {{app}}. Intenta de nuevo o inicia en la aplicación web {{app}}.",
"description": "¿No pasó nada? Intentamos iniciar la reunión en la aplicación de escritorio {{app}}. Intenta de nuevo o inicia en la aplicación web {{app}}.",
"descriptionNew": "¿No pasó nada? Intentamos iniciar la reunión en la aplicación de escritorio {{app}}. <br /><br /> Puedes volver a intentarlo o iniciar en la aplicación web.",
"descriptionWithoutWeb": "¿No pasó nada? Intentamos iniciar su reunión en la aplicación de escritorio {{app}}.",
"downloadApp": "Descargar la app",
"downloadMobileApp": "",
"ifDoNotHaveApp": "Si aún no tienes la app:",
"ifHaveApp": "Si ya tienes la app:",
"joinInApp": "Unirse a la reunion usando la app",
"joinInAppNew": "Unirse en la app",
"joinInBrowser": "Unirse en el navegador",
"launchMeetingLabel": "¿Cómo quieres unirte a la reunión?",
"launchWebButton": "Iniciar en el navegador",
"noMobileApp": "¿No tienes la aplicación?",
"termsAndConditions": "Al continuar aceptas nuestros <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>términos y condiciones.</a>",
"title": "Iniciando la reunión en {{app}}…",
"tryAgainButton": "Intentar de nuevo en el escritorio"
"titleNew": "Iniciando la reunión.",
"tryAgainButton": "Intentar de nuevo en el escritorio",
"unsupportedBrowser": "Parece que estás usando un navegador para el que no tenemos soporte."
},
"defaultLink": "ej. {{url}}",
"defaultNickname": "ej. Juan Pérez",
@@ -169,11 +225,20 @@
"microphonePermission": "Error al obtener permiso del micrófono"
},
"deviceSelection": {
"hid": {
"callControl": "Control de llamadas",
"connectedDevices": "Dispositivos conectados:",
"deleteDevice": "Eliminar dispositivo",
"pairDevice": "Emparejar dispositivo"
},
"noPermission": "Permiso no concedido",
"previewUnavailable": "Vista previa no disponible",
"selectADevice": "Seleccionar un dispositivo",
"testAudio": "Reproducir un sonido de prueba"
},
"dialIn": {
"screenTitle": ""
},
"dialOut": {
"statusMessage": "está {{status}}"
},
@@ -189,9 +254,13 @@
"WaitingForHostTitle": "Esperando al anfitrión...",
"Yes": "Sí",
"accessibilityLabel": {
"liveStreaming": "Transmisión en vivo"
"close": "Cerrar diálogo",
"liveStreaming": "Transmisión en vivo",
"sharingTabs": "Opciones para compartir"
},
"add": "Agregar",
"addMeetingNote": "Agrega una nota acerca de esta reunión",
"addOptionalNote": "Agrega una nota (opcional):",
"allow": "Permitir",
"alreadySharedVideoMsg": "Otro participante ya está compartiendo un vídeo. Esta conferencia sólo permite compartir un vídeo a la vez.",
"alreadySharedVideoTitle": "Solo se permite un vídeo compartido a la vez",
@@ -233,6 +302,7 @@
"gracefulShutdown": "Nuestro servicio se encuentra en mantenimiento. Por favor, intente más tarde.",
"grantModeratorDialog": "¿Estás seguro de que quieres convertir a este participante en moderador?",
"grantModeratorTitle": "Convertir en moderador",
"hide": "Esconder",
"hideShareAudioHelper": "No volver a mostrar este diálogo",
"incorrectPassword": "Nombre de usuario o contraseña incorrecta",
"incorrectRoomLockPassword": "Contraseña incorrecta",
@@ -243,9 +313,10 @@
"kickParticipantDialog": "¿Seguro que quiere expulsar a este participante?",
"kickParticipantTitle": "¿Expulsar a este participante?",
"kickTitle": "¡Ay! {{participantDisplayName}} te expulsó de la reunión",
"linkMeeting": "",
"linkMeetingTitle": "",
"liveStreaming": "Transmisión en vivo",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "No es posible mientras la grabación este activa",
"liveStreamingDisabledTooltip": "Las trasmisiones están deshabilitadas.",
"localUserControls": "Controles de usuario locales",
"lockMessage": "No se pudo bloquear la conferencia.",
"lockRoom": "Agregar $t(lockRoomPasswordUppercase) a la reunión",
@@ -279,11 +350,11 @@
"muteEveryonesVideoTitle": "¿Detener el vídeo de todos?",
"muteParticipantBody": "No podrás quitarles el modo en silencio, pero ellos pueden quitárselo en cualquier momento.",
"muteParticipantButton": "Silenciar",
"muteParticipantDialog": "¿Seguro que quieres silenciar a este participante? No podrás revertir esta acción, pero el participante podrá hacerlo en cualquier momento",
"muteParticipantTitle": "¿Silenciar a este participante?",
"muteParticipantsVideoBody": "No podrás volver a encender la cámara, pero ellos pueden volver a encenderla en cualquier momento.",
"muteParticipantsVideoBodyModerationOn": "",
"muteParticipantsVideoButton": "Detener video",
"muteParticipantsVideoDialog": "¿Estás seguro de que quieres apagar la cámara de este participante? No podrás volver a encender la cámara, pero ellos pueden volver a encenderla en cualquier momento.",
"muteParticipantsVideoDialogModerationOn": "",
"muteParticipantsVideoTitle": "¿Desactivar la cámara de este participante?",
"noDropboxToken": "No hay un token válido de Dropbox",
"password": "Contraseña",
@@ -297,9 +368,9 @@
"popupError": "Su navegador está bloqueando las ventanas emergentes de este sitio. Habilite las ventanas emergentes en la configuración de seguridad de su navegador y vuelva a intentarlo.",
"popupErrorTitle": "Ventana emergente bloqueada",
"readMore": "más",
"recentlyUsedObjects": "Tus objetos usados recientemente",
"recording": "Grabando",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "No es posible mientras la transmisión en vivo este activa",
"recordingDisabledTooltip": "Inicio de grabación desactivado.",
"rejoinNow": "Reunirse ahora",
"remoteControlAllowedMessage": "¡{{user}} ha aceptado tu solicitud de control remoto!",
"remoteControlDeniedMessage": "¡{{user}} ha rechazado tu solicitud de control remoto!",
@@ -319,6 +390,12 @@
"screenSharingFailed": "¡Ups! ¡Algo salió mal, no se pudo iniciar la compartición de su pantalla!",
"screenSharingFailedTitle": "¡Fallo al compartir su pantalla!",
"screenSharingPermissionDeniedError": "¡Uy! Algo salió mal con tus permisos de extensión para compartir pantalla. Vuelve a cargar la página e intenta de nuevo.",
"searchInSalesforce": "Buscar en Salesforce",
"searchResults": "Resultados de búsqueda({{count}}",
"searchResultsDetailsError": "",
"searchResultsError": "Hubo un error recuperando los datos.",
"searchResultsNotFound": "No se encontraron resultados.",
"searchResultsTryAgain": "Vuelve a intentar usando palabras clave alternativas",
"sendPrivateMessage": "Acabas de recibir un mensaje privado. ¿Deseas responder en privado o a todos?",
"sendPrivateMessageCancel": "Enviar al grupo",
"sendPrivateMessageOk": "Enviar en privado",
@@ -341,7 +418,10 @@
"shareVideoTitle": "Compartir un vídeo",
"shareYourScreen": "Compartir pantalla",
"shareYourScreenDisabled": "Se desactivó la opción para compartir pantalla.",
"sharedVideoDialogError": "Error: URL inválido",
"sharedVideoLinkPlaceholder": "Enlace de YouTube o enlace de vídeo directo",
"show": "Mostrar",
"start": "Iniciar",
"startLiveStreaming": "Iniciar transmisión en vivo",
"startRecording": "Iniciar grabación",
"startRemoteControlErrorMessage": "Se produjo un error al intentar iniciar la sesión de control remoto.",
@@ -359,6 +439,10 @@
"user": "Usuario",
"userIdentifier": "Identificador de usuario",
"userPassword": "contraseña del usuario",
"verifyParticipantConfirm": "",
"verifyParticipantDismiss": "",
"verifyParticipantQuestion": "",
"verifyParticipantTitle": "Verificación de usuario",
"videoLink": "Enlace de vídeo",
"viewUpgradeOptions": "Ver opciones de mejora",
"viewUpgradeOptionsContent": "Para obtener acceso ilimitado a las funciones premium, como la grabación, las transcripciones, el streaming RTMP y otras, tendrás que actualizar tu plan.",
@@ -384,8 +468,14 @@
"veryBad": "Muy mala",
"veryGood": "Muy buena"
},
"helpView": {
"title": "Centro de ayuda"
"filmstrip": {
"accessibilityLabel": {
"heading": "Miniaturas de video"
}
},
"giphy": {
"noResults": "No se encontraron resultados :(",
"search": "Busca en GIPHY"
},
"incomingCall": {
"answer": "Contestar",
@@ -427,9 +517,11 @@
"noRoom": "No se especificó la sala a marcar.",
"numbers": "Números para entrar por llamada telefónica:",
"password": "$t(lockRoomPasswordUppercase):",
"reachedLimit": "Alcanzaste el límite de tu plan.",
"sip": "Dirección SIP",
"title": "Compartir",
"tooltip": "Compartir el enlace y acceso telefónico para esta reunión"
"tooltip": "Compartir el enlace y acceso telefónico para esta reunión",
"upgradeOptions": "Por favor revisa las opciones de mejora en"
},
"inlineDialogFailure": {
"msg": "Tuvimos un pequeño tropiezo.",
@@ -450,6 +542,7 @@
"focusLocal": "Ver tu cámara",
"focusRemote": "Ver la cámara de otras personas",
"fullScreen": "Entrar o salir de pantalla completa",
"giphyMenu": "Alternar menú GIPHY",
"keyboardShortcuts": "Atajos de teclado",
"localRecording": "Mostrar u ocultar controles de grabación local",
"mute": "Activar o silenciar el micrófono",
@@ -463,6 +556,10 @@
"toggleShortcuts": "Mostrar u ocultar atajos del teclado",
"videoMute": "Encender o apagar la cámara"
},
"largeVideo": {
"screenIsShared": "Estás compartiendo tu pantalla",
"showMeWhatImSharing": "Muéstrame qué estoy compartiendo"
},
"liveStreaming": {
"busy": "Nuestros servidores andan un poco ocupados. Vuelve a intentarlo en unos minutos.",
"busyTitle": "Todos los transmisores están ocupados",
@@ -479,6 +576,7 @@
"failedToStart": "La transmisión en vivo no se pudo iniciar",
"getStreamKeyManually": "No pudimos encontrar tu clave de transmisión. Por favor, obtenla de la página de YouTube y pégala.",
"googlePrivacyPolicy": "Política de Privacidad de Google",
"inProgress": "Grabación o transmisión en vivo en curso",
"invalidStreamKey": "Es posible que la clave de transmisión sea incorrecta, o no es de YouTube.",
"limitNotificationDescriptionNative": "Su transmisión estará limitada a {{limit}} minutos. Puede obtener transmisiones ilimitadas en {{app}}.",
"limitNotificationDescriptionWeb": "Debido a la alta demanda su transmisión estará limitada a {{limit}} minutos. Puede obtener transmisiones ilimitadas en <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
@@ -488,6 +586,7 @@
"onBy": "{{name}} inició la transmisión en vivo",
"pending": "Iniciando transmisión en vivo…",
"serviceName": "Servicio de transmisión en vivo",
"sessionAlreadyActive": "Esta sesión ya está siendo grabada o transmitida en vivo.",
"signIn": "Iniciar sesión con Google",
"signInCTA": "Para transmitir a YouTube, inicia sesión o introduce la clave de transmisión. Para transmitir a otro lugar, introduce el URL (que empieza en rtmp), seguido de la clave de transmisión. Debe haber una diagonal (/) entre ambos.",
"signOut": "Cerrar sesión",
@@ -501,8 +600,8 @@
"lobby": {
"admit": "Admitir",
"admitAll": "Admitir todo",
"allow": "permitir",
"backToKnockModeButton": "No hay contraseña, pide permiso para entrar.",
"chat": "Chat",
"dialogTitle": "Sala de espera",
"disableDialogContent": "Sala de espera activada. Así no entrarán intrusos. ¿Quieres desactivarla?",
"disableDialogSubmit": "Desactivar",
@@ -515,6 +614,7 @@
"errorMissingPassword": "Por favor, introduzca la contraseña de la reunión",
"invalidPassword": "Contraseña inválida",
"joinRejectedMessage": "Tu solicitud para entrar ha sido rechazada por un moderador.",
"joinRejectedTitle": "Solicitud para entrar rechazada.",
"joinTitle": "Entrar a la reunión",
"joinWithPasswordMessage": "Tratando de entrar con contraseña, por favor espera...",
"joiningMessage": "Podrás entrar tan pronto te acepten tu solicitud.",
@@ -523,6 +623,8 @@
"knockButton": "Pedir entrar",
"knockTitle": "Alguien quiere entrar a la reunión",
"knockingParticipantList": "Participantes que quieren entrar",
"lobbyChatStartedNotification": "{{moderator}} inició un chat de lobby con {{attendee}}",
"lobbyChatStartedTitle": "{{moderator}} inició un chat de lobby contigo.",
"nameField": "Introduce tu nombre",
"notificationLobbyAccessDenied": "{{originParticipantName}} no dejó entrar a {{targetParticipantName}}",
"notificationLobbyAccessGranted": "{{originParticipantName}} permitió entrar a {{targetParticipantName}}",
@@ -560,6 +662,7 @@
"no": "No",
"participant": "Participante",
"participantStats": "Estadística de participantes",
"selectTabTitle": "🎥 Por favor seleccione esta pestaña para grabar",
"sessionToken": "Token de sesión",
"start": "Iniciar grabación",
"stop": "Detener grabación",
@@ -576,18 +679,39 @@
"OldElectronAPPTitle": "¡Aplicación obsoleta e insegura!",
"allowAction": "Permitir",
"allowedUnmute": "Puedes anular el silencio del micrófono, iniciar la cámara o compartir la pantalla.",
"audioUnmuteBlockedDescription": "La operación de activación del micrófono ha sido bloqueada temporalmente debido a límites del sistema.",
"audioUnmuteBlockedTitle": "¡Activación del micrófono bloqueado!",
"chatMessages": "Mensajes del chat",
"connectedOneMember": "{{name}} se unió a la reunión",
"connectedThreePlusMembers": "{{name}} y {{count}} más se unieron a la reunión",
"connectedTwoMembers": "{{first}} y {{second}} se unieron a la reunión",
"dataChannelClosed": "",
"dataChannelClosedDescription": "",
"disabledIframe": "",
"disconnected": "desconectado",
"displayNotifications": "Mostrar notificaciones para",
"dontRemindMe": "No me lo recuerdes",
"focus": "Enfocar conferencia",
"focusFail": "{{component}} no disponible. Vuelve a intentar en {{ms}} segundos",
"gifsMenu": "GIPHY",
"groupTitle": "Notificaciones",
"hostAskedUnmute": "El moderador quiere que hables",
"invitedOneMember": "{{name}} ha sido invitado",
"invitedThreePlusMembers": "{{name}} y {{count}} más han sido invitados",
"invitedTwoMembers": "{{first}} y {{second}} han sido invitados",
"joinMeeting": "Unirse",
"kickParticipant": "{{kicker}} sacó a {{kicked}}",
"leftOneMember": "{{name}} abandonó la reunión",
"leftThreePlusMembers": "{{name}} y muchos otros abandonaron la reunión",
"leftTwoMembers": "{{first}} y {{second}} abandonaron la reunión",
"linkToSalesforce": "Enlace a Salesforce",
"linkToSalesforceDescription": "Puedes vincular el resumen de la reunión a un objeto Salesforce",
"linkToSalesforceError": "Error al vincular la reunión a Salesforce",
"linkToSalesforceKey": "",
"linkToSalesforceProgress": "Vinculando reunión a Salesorce...",
"linkToSalesforceSuccess": "La reunión fue vinculada a Salesforce",
"localRecordingStarted": "{{name}} ha iniciado una grabación local.",
"localRecordingStopped": "{{name}} ha detenido una grabación local.",
"me": "Yo",
"moderationInEffectCSDescription": "Por favor, levante la mano si quiere compartir su pantalla.",
"moderationInEffectCSTitle": "La pantalla compartida está bloqueada por el moderador",
@@ -608,16 +732,27 @@
"newDeviceAction": "Usar",
"newDeviceAudioTitle": "Se detectó un dispositivo de audio nuevo",
"newDeviceCameraTitle": "Se detectó una cámara nueva",
"noiseSuppressionDesktopAudioDescription": "La supresión de ruido no puede ser habilitada mientras comparte audio del escritorio, por favor deshabilítelo y vuelva a intentar.",
"noiseSuppressionFailedTitle": "Error al activar la supresión de ruido",
"noiseSuppressionNoTrackDescription": "Por favor active su micrófono primero.",
"noiseSuppressionStereoDescription": "La supresión de ruido en audio estéreo no tiene soporte actualmente",
"oldElectronClientDescription1": "Estás usando una versión vieja de la aplicación de Jitsi Meet que tiene problemas de seguridad. ¡Por favor, actualiza a la ",
"oldElectronClientDescription2": "versión más reciente",
"oldElectronClientDescription3": " YA!",
"participantWantsToJoin": "Quiere unirse a la reunión",
"participantsWantToJoin": "Quieren unirse a la reunión",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) eliminada por otro participante",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) agregada por otro participante",
"raiseHandAction": "Levantar la mano",
"raisedHand": "{{name}} quisiera hablar.",
"raisedHands": "",
"reactionSounds": "Desactivar sonidos",
"reactionSoundsForAll": "Desactivar sonidos para todos",
"screenShareNoAudio": "La casilla Compartir audio no estaba marcada en la pantalla de selección de ventanas.",
"screenShareNoAudioTitle": "No se pudo compartir el audio del sistema.",
"screenSharingAudioOnlyDescription": "Por favor tenga en cuenta que al compartir si pantalla está afectando el modo \"Mejor rendimiento\" y usará más ancho de banda",
"screenSharingAudioOnlyTitle": "Modo \"Mejor rendimiento\"",
"selfViewTitle": "Siempre puedes reactivar la vista propia en los ajustes",
"somebody": "Alguien",
"startSilentDescription": "Vuelve a ingresar para activar el audio",
"startSilentTitle": "¡Te uniste sin audio!",
@@ -625,7 +760,11 @@
"suboptimalExperienceTitle": "¡Tu navegador no es compatible!",
"unmute": "Reactivar micrófono",
"videoMutedRemotelyDescription": "Siempre puedes volver a encenderlo.",
"videoMutedRemotelyTitle": "Su vídeo ha sido desactivado por {{moderator}}"
"videoMutedRemotelyTitle": "Su vídeo ha sido desactivado por {{moderator}}",
"videoUnmuteBlockedDescription": "Las operaciones de desactivar la cámara y compartir pantalla hansido bloqueadas temporalmente debido a límites del sistema.",
"videoUnmuteBlockedTitle": "¡Desactivar cámara y compartir pantalla bloqueados!",
"viewLobby": "Ver lobby",
"waitingParticipants": "{{waitingParticipants}} personas"
},
"participantsPane": {
"actions": {
@@ -635,6 +774,9 @@
"audioModeration": "Desmutearse a sí mismos",
"blockEveryoneMicCamera": "Bloquear el micrófono y la cámara de todos.",
"invite": "Invitar a alguien",
"moreModerationActions": "Más opciones de moderación",
"moreModerationControls": "Más controles de moderación",
"moreParticipantOptions": "Más opciones de participantes",
"mute": "Silenciar",
"muteAll": "Silenciar a todos los demás",
"muteEveryoneElse": "Silenciar al resto",
@@ -647,17 +789,22 @@
"headings": {
"lobby": "Vestíbulo ({{count}})",
"participantsList": "Participantes en la reunión ({{count}})",
"visitors": "Visitantes ({{count}})",
"waitingLobby": "Esperando en el vestíbulo ({{count}})"
},
"search": "Buscar participantes",
"title": "Participantes"
},
"passwordDigitsOnly": "Hasta {{number}} cifras",
"passwordSetRemotely": "Definida por otro participante",
"pinParticipant": "",
"pinnedParticipant": "",
"polls": {
"answer": {
"skip": "Saltar",
"submit": "Enviar"
},
"by": "Por {{ name }}",
"create": {
"addOption": "Añadir opción",
"answerPlaceholder": "Opción {{index}}",
@@ -728,15 +875,18 @@
"initiated": "Llamada iniciada",
"joinAudioByPhone": "Entrar con audio de llamada telefónica",
"joinMeeting": "Entrar a la reunión",
"joinMeetingInLowBandwidthMode": "Entrar en modo de ancho de banda bajo",
"joinWithoutAudio": "Entrar sin sonido",
"keyboardShortcuts": "Activar los atajos de teclado",
"linkCopied": "Se copió el link",
"lookGood": "Tu micrófono funciona bien.",
"or": "o",
"premeeting": "Pre-reunión",
"proceedAnyway": "Continuar de todos modos",
"screenSharingError": "Error al compartir pantalla:",
"showScreen": "Habilitar pantalla pre-reunión",
"startWithPhone": "Iniciar con audio de llamada telefónica",
"unsafeRoomConsent": "Comprendo los riesgos, quiero unirme a la reunión",
"videoOnlyError": "Error con el vídeo:",
"videoTrackError": "No se pudo crear la pista de vídeo.",
"viewAllNumbers": "ver todos los números"
@@ -763,6 +913,19 @@
"title": "Perfil"
},
"raisedHand": "Desea hablar",
"raisedHandsLabel": "Cantidad de manos levantadas",
"record": {
"already": {
"linked": "La reunión ya está vinculada a este objeto Salesforce"
},
"type": {
"account": "Cuenta",
"contact": "Contacto",
"lead": "",
"opportunity": "Oportunidad",
"owner": "Dueño"
}
},
"recording": {
"authDropboxText": "Subir a Dropbox",
"availableSpace": "Espacio disponible: {{spaceLeft}} MB (aproximadamente {{duration}} minutos de grabación)",
@@ -777,37 +940,66 @@
"expandedPending": "La grabación se está iniciando…",
"failedToStart": "No se pudo iniciar la grabación",
"fileSharingdescription": "Compartir la grabación con los participantes de la reunión",
"highlight": "Destacar",
"highlightMoment": "Destacar momento",
"highlightMomentDisabled": "Puede destacar momentos cuando inicie la grabación",
"highlightMomentSuccess": "Momento destacado",
"highlightMomentSucessDescription": "Su momento destacado será agregado al resumen de la reunión.",
"inProgress": "Grabación o transmisión en vivo en curso",
"limitNotificationDescriptionNative": "Su grabación estará limitada a {{limit}} minutos. Puede obtener grabaciones ilimitadas en <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Debido a la alta demanda su grabación estará limitada a {{limit}} minutos. Puede obtener grabaciones ilimitadas en <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "Hemos generado un enlace a su grabación.",
"live": "EN VIVO",
"localRecordingNoNotificationWarning": "La grabación no será anunciada al resto de participantes. Necesitarás hacerles saber que la reunión está siendo grabada.",
"localRecordingNoVideo": "El video no está siendo grabado",
"localRecordingStartWarning": "Por favor asegúrese de detener la grabación antes de abandonar la reunión para guardarla.",
"localRecordingStartWarningTitle": "Detenga la grabación para guardarla",
"localRecordingVideoStop": "Detener su video también detendrá la grabación local. ¿Está seguro de querer continuar?",
"localRecordingVideoWarning": "Para grabar su video debe tenerlo encendido al iniciar la grabación",
"localRecordingWarning": "Asegúrese de seleccionar la pestaña actual para usar el video y audio correctos. La grabación está actualmente limitada a 1GB, que son aproximadamente 100 minutos.",
"loggedIn": "Sesión iniciada como {{userName}}",
"noMicPermission": "No se pudo crear la pista de micrófono. Por favor otorgue permiso para usar el micrófono.",
"noStreams": "",
"off": "Grabación detenida",
"offBy": "{{name}} detuvo la grabación",
"on": "Grabando",
"onBy": "{{name}} comenzó la grabación",
"onlyRecordSelf": "",
"pending": "Preparando para grabar la reunión…",
"rec": "GRA",
"saveLocalRecording": "Guardar archivo de grabación localmente (Beta)",
"serviceDescription": "El servicio de grabación guardará la grabación",
"serviceDescriptionCloud": "Grabación en la nube",
"serviceDescriptionCloudInfo": "Las reuniones grabadas son limpiadas 24h luego de su horario de grabación.",
"serviceName": "Servicio de grabación",
"sessionAlreadyActive": "Esta sesión ya está siendo grabada o transmitida en vivo.",
"signIn": "Iniciar sesión",
"signOut": "Cerrar sesión",
"surfaceError": "Por favor seleccione la pestaña actual.",
"title": "Grabando",
"unavailable": "¡Uy! {{serviceName}} actualmente no está disponible. Estamos trabajando para resolver el problema. Vuelve a intentarlo más tarde.",
"unavailableTitle": "Grabación no disponible",
"uploadToCloud": "Subir a la nube"
},
"screenshareDisplayName": "Pantalla de {{name}}",
"sectionList": {
"pullToRefresh": "Mueve el dedo para abajo para actualizar."
},
"security": {
"about": "Puedes agregar una contraseña a la reunión. Los participantes necesitarán la contraseña para unirse a la reunión.",
"aboutReadOnly": "Los participantes moderadores pueden agregar una $t(lockRoomPassword) a la reunión. Los participantes deberán proporcionar la $t(lockRoomPassword) antes de que se les permita unirse a la reunión.",
"insecureRoomNameWarning": "El nombre de la sala es inseguro. Participantes no deseados pueden llegar a unirse a la reunión.",
"securityOptions": "Opciones de seguridad"
"insecureRoomNameWarningNative": "El nombre de esta sala es inseguro. Participantes indeseados podrían ingresar a su reunión. {{recommendAction}} Aprenda más sobre asegurar su reunión ",
"insecureRoomNameWarningWeb": "El nombre de esta sala es inseguro. Participantes indeseados podrían ingresar a su reunión. {{recommendAction}} Aprenda más sobre asegurar su reunión <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">aquí</a>.",
"title": "Opciones de seguridad",
"unsafeRoomActions": {
"meeting": "Considere hacer más segura su reunión utilizando el botón de seguridad.",
"prejoin": "Considere utilizar un nombre de reunión más único.",
"welcome": "Considere utilizar un nombre de reunión más único, o elija una de las sugerencias"
}
},
"settings": {
"audio": "Audio",
"buttonLabel": "Ajustes",
"calendar": {
"about": "La integración del calendario de {{appName}} se usa para acceder al calendario de manera segura para que puedas estar al tanto de los próximos eventos.",
"disconnect": "Desconectar",
@@ -824,12 +1016,16 @@
"incomingMessage": "Mensaje entrante",
"language": "Idioma",
"loggedIn": "Sesión iniciada como {{name}}",
"maxStageParticipants": "",
"microphones": "Micrófono",
"moderator": "Moderador",
"moderatorOptions": "Opciones de moderador",
"more": "Más",
"name": "Nombre",
"noDevice": "Ninguno",
"participantJoined": "Un articipante incorporado",
"notifications": "Notificaciones",
"participantJoined": "Un participante se ha unido",
"participantKnocking": "Un participante ha ingresado al lobby",
"participantLeft": "Un participante se ha ido",
"playSounds": "Reproducir sonido",
"reactions": "Reacciones de la reunión",
@@ -837,12 +1033,15 @@
"selectAudioOutput": "Salida de audio",
"selectCamera": "Cámara",
"selectMic": "Micrófono",
"sounds": "Sonidos",
"selfView": "Vista propia",
"shortcuts": "Atajos",
"speakers": "Altavoces",
"startAudioMuted": "Todos inician silenciados",
"startReactionsMuted": "Silenciar sonidos de reacción para todos",
"startVideoMuted": "Todos inician con cámara desactivada",
"talkWhileMuted": "Hablar en silencio",
"title": "Ajustes"
"title": "Ajustes",
"video": "Video"
},
"settingsView": {
"advanced": "Avanzado",
@@ -857,13 +1056,21 @@
"disableCrashReportingWarning": "¿Estás seguro que no deseas reportarnos los crasheos? La opción se activará al reiniciar la app.",
"disableP2P": "Desactivar la comunicación directa (\"Peer-To-Peer\")",
"displayName": "Nombre a mostrar",
"displayNamePlaceholderText": "Por ejemplo: Juan Pérez",
"email": "Correo electrónico",
"emailPlaceholderText": "",
"goTo": "Ir a",
"header": "Configuración",
"help": "Ayuda",
"links": "Enlaces",
"privacy": "Privacidad",
"profileSection": "Perfil",
"serverURL": "URL del servidor",
"showAdvanced": "Mostrar configuración avanzada",
"startCarModeInLowBandwidthMode": "Iniciar módo automóvil en modo ancho de banda bajo",
"startWithAudioMuted": "Iniciar con el micrófono apagado",
"startWithVideoMuted": "Iniciar con la cámara apagada",
"terms": "Términos",
"version": "Versión"
},
"share": {
@@ -872,13 +1079,21 @@
},
"speaker": "Participante",
"speakerStats": {
"angry": "Enojado",
"disgusted": "Disgustado",
"displayEmotions": "Mostrar emociones",
"fearful": "Temeroso",
"happy": "Feliz",
"hours": "{{count}} h",
"minutes": "{{count}} min",
"name": "Nombre",
"neutral": "Neutral",
"sad": "Triste",
"search": "Buscar",
"seconds": "{{count}} s",
"speakerStats": "Estadísticas de participantes",
"speakerTime": "Tiempo hablado"
"speakerTime": "Tiempo hablado",
"surprised": "Sorprendido"
},
"startupoverlay": {
"genericTitle": "La reunión debe utilizar su micrófono y su cámara.",
@@ -890,6 +1105,10 @@
"text": "Presiona el botón <i>Reconectar</i> para volver a conectarte.",
"title": "La vídeollamada se interrumpió porque la computadora estaba suspendida."
},
"termsView": {
"title": "Términos"
},
"toggleTopPanelLabel": "Alternar panel superior",
"toolbar": {
"Settings": "Configuración",
"accessibilityLabel": {
@@ -897,60 +1116,89 @@
"audioOnly": "Alternar cámaras de los demás",
"audioRoute": "Seleccionar el dispositivo de sonido",
"boo": "Boo",
"breakoutRoom": "Unirse/abandonar sala para grupos pequeños",
"callQuality": "Administrar la calidad de vídeo",
"carmode": "Modo automóvil",
"cc": "Alternar subtítulos",
"chat": "Alternar ventana de chat",
"clap": "Aplauso",
"closeChat": "Cerrar chat",
"closeMoreActions": "Cerrar el menú de más acciones",
"closeParticipantsPane": "Cerrar panel de participantes",
"collapse": "Colapsar",
"document": "Alternar documento compartido",
"documentClose": "Cerrar documento compartido",
"documentOpen": "Abrir documento compartido",
"download": "Descargar nuestras aplicaciones",
"embedMeeting": "Insertar reunión",
"endConference": "Terminar reunión para todos",
"enterFullScreen": "Ver en pantalla completa",
"enterTileView": "Ingresar en vista de mosaico",
"exitFullScreen": "Salir de pantalla completa",
"exitTileView": "Salir de vista de mosaico",
"expand": "Ampliar",
"feedback": "Dejar comentarios",
"fullScreen": "Alternar pantalla completa",
"giphy": "Alternar menú GIPHY",
"grantModerator": "Convertir en moderador",
"hangup": "Colgar",
"heading": "Barra de herramientas",
"help": "Ayuda",
"hideWhiteboard": "Esconder pizarra",
"invite": "Invitar personas",
"kick": "Expulsar participante",
"laugh": "Ríete",
"leaveConference": "Abandonar reunión",
"like": "Pulgares arriba",
"linkToSalesforce": "Enlace a Salesforce",
"lobbyButton": "Activar / desactivar el modo lobby",
"localRecording": "Alternar controles de grabación local",
"lockRoom": "Alternar contraseña de la reunión",
"lowerHand": "Bajar mano",
"moreActions": "Alternar más acciones",
"moreActionsMenu": "Menú de más acciones",
"moreOptions": "Mostrar más opciones",
"mute": "Silenciar micrófono",
"muteEveryone": "Silenciar a todos",
"muteEveryoneElse": "Silenciar a todos los demás",
"muteEveryoneElsesVideo": "Desactivar el vídeo de los demás",
"muteEveryonesVideo": "Desactivar el vídeo de todos",
"muteEveryoneElsesVideoStream": "Detener el video del resto",
"muteEveryonesVideoStream": "Detener el video de todos",
"muteGUMPending": "Conectando su micrófono",
"noiseSuppression": "Supresión de ruido",
"openChat": "Abrir chat",
"participants": "Participantes",
"pip": "Alternar modo ventana en miniatura",
"privateMessage": "Enviar mensaje privado",
"profile": "Editar perfil",
"raiseHand": "Levantar o bajar la mano",
"reactions": "Reacciones",
"reactionsMenu": "Abrir / Cerrar el menú de reacciones",
"recording": "Alternar grabación",
"remoteMute": "Silenciar participante",
"remoteVideoMute": "Desactivar la cámara del participante",
"security": "Opciones de seguridad",
"selectBackground": "Seleccione el fondo",
"selfView": "Alternar vista propia",
"shareRoom": "Invitar a alguien",
"shareYourScreen": "Comenzar / detener compartir pantalla",
"shareaudio": "Compartir audio",
"sharedvideo": "Alternar vídeo compartido",
"shortcuts": "Alternar accesos directos",
"show": "Mostrar en primer",
"showWhiteboard": "Mostrar vista propia",
"silence": "Silencio",
"speakerStats": "Alternar estadísticas del orador",
"stopScreenSharing": "Dejar de compartir pantalla",
"stopSharedVideo": "Detener video",
"surprised": "Sorprendido",
"tileView": "Alternar vista de mosaico",
"toggleCamera": "Alternar cámara",
"toggleFilmstrip": "Alternar mosaicos",
"unmute": "Activar micrófono",
"videoblur": "Alternar desenfoque de vídeo",
"videomute": "Alternar vídeo"
"videomute": "Alternar vídeo",
"videomuteGUMPending": "Conectando tu cámara",
"videounmute": "Encender cámara"
},
"addPeople": "Agregar personas a la llamada",
"audioOnlyOff": "Mostrar cámaras de los demás",
@@ -963,23 +1211,33 @@
"chat": "Abrir o cerrar chat",
"clap": "Aplauso",
"closeChat": "Cerrar chat",
"closeParticipantsPane": "Cerrar panel de participantes",
"closeReactionsMenu": "Cerrar el menú de reacciones",
"disableNoiseSuppression": "Desactivar supresión de ruido",
"disableReactionSounds": "Puede desactivar los sonidos de reacción para esta reunión",
"documentClose": "Cerrar documento compartido",
"documentOpen": "Abrir documento compartido",
"download": "Descarga nuestras aplicaciones",
"e2ee": "Cifrado de extremo a extremo",
"embedMeeting": "Insertar reunión",
"enableNoiseSuppression": "Activar supresión de ruido",
"endConference": "Terminar reunión para todos",
"enterFullScreen": "Pantalla completa",
"enterTileView": "Ver en cuadrícula",
"exitFullScreen": "Salir de pantalla completa",
"exitTileView": "Salir de vista de mosaico",
"feedback": "Dejar sugerencias",
"giphy": "Alternar menú GIPHY",
"hangup": "Colgar",
"help": "Ayuda",
"hideWhiteboard": "Esconder pizarra",
"invite": "Invitar personas",
"joinBreakoutRoom": "Unirse a sala para grupos pequeños",
"laugh": "Ríete",
"leaveBreakoutRoom": "Abandonar sala para grupos pequeños",
"leaveConference": "Abandonar reunión",
"like": "Pulgares arriba",
"linkToSalesforce": "",
"lobbyButtonDisable": "Desactivar el modo lobby",
"lobbyButtonEnable": "Activar el modo lobby",
"login": "Inicio de sesión",
@@ -990,11 +1248,13 @@
"mute": "Activar o silenciar el micrófono",
"muteEveryone": "Silenciar a todos",
"muteEveryonesVideo": "Desactivar la cámara de todos",
"muteGUMPending": "Conectando tu micrónono",
"noAudioSignalDesc": "Checa si no está silenciado en tu configuración del sistema o dispositivo, o cambia de micrófono.",
"noAudioSignalDescSuggestion": "Si no lo silenciaste a propósito desde la configuración del sistema o el dispositivo, intenta usar este otro micrófono:",
"noAudioSignalDialInDesc": "Además, puedes llamar usando:",
"noAudioSignalDialInLinkDesc": "Números de llamada",
"noAudioSignalTitle": "¡No se registra audio de tu micrófono!",
"noiseSuppression": "Supresión de ruido",
"noisyAudioInputDesc": "Tu micrófono está haciendo ruido, siléncialo, ajusta su volumen en configuración del sistema, o cambia de micrófono.",
"noisyAudioInputTitle": "Tu micrófono parece estar ruidoso",
"openChat": "Abrir chat",
@@ -1011,12 +1271,14 @@
"reactionLike": "Enviar la reacción de los pulgares hacia arriba",
"reactionSilence": "Enviar reacción de silencio",
"reactionSurprised": "Enviar reacción de sorpresa",
"reactions": "Reacciones",
"security": "Opciones de seguridad",
"selectBackground": "Seleccionar fondo",
"shareRoom": "Invitar a alguien",
"shareaudio": "Compartir audio",
"sharedvideo": "Compartir un vídeo",
"shortcuts": "Ver atajos del teclado",
"showWhiteboard": "Mostrar pizarra",
"silence": "Silencio",
"speakerStats": "Estadísticas de los participantes",
"startScreenSharing": "Comenzar a compartir pantalla",
@@ -1029,8 +1291,11 @@
"talkWhileMutedPopup": "¿Intentas hablar? Estás silenciado.",
"tileViewToggle": "Activar o desactivar vista en cuadrícula",
"toggleCamera": "Alternar cámara",
"unmute": "Activar",
"videoSettings": "Ajustes de vídeo",
"videomute": "Iniciar o detener cámara"
"videomute": "Detener cámara",
"videomuteGUMPending": "Conectando tu cámara",
"videounmute": "Iniciar cámara"
},
"transcribing": {
"ccButtonTooltip": "Iniciar o detener subtítulos",
@@ -1040,10 +1305,15 @@
"labelToolTip": "La reunión se está transcribiendo",
"off": "Transcripción detenida",
"pending": "Preparando para transcribir la reunión…",
"sourceLanguageDesc": "El lenguaje actual de la reunión es <b>{{sourceLanguage}}</b>. <br/> Puedes cambiarlo desde ",
"sourceLanguageHere": "aquí",
"start": "Mostrar subtítulos",
"stop": "Dejar de mostrar subtítulos",
"subtitles": "Subtítulos",
"subtitlesOff": "",
"tr": "TR"
},
"unpinParticipant": "",
"userMedia": {
"androidGrantPermissions": "Selecciona <b><i>Permitir</i></b> cuando el navegador solicite permisos.",
"chromeGrantPermissions": "Selecciona <b><i>Permitir</i></b> cuando el navegador solicite permisos.",
@@ -1067,20 +1337,26 @@
"pending": "{{displayName}} ha sido invitado"
},
"videoStatus": {
"adjustFor": "Ajustar para:",
"audioOnly": "AUD",
"audioOnlyExpanded": "Estás en modo de ancho de banda bajo. En este modo, sólo recibirás audio y pantalla compartida.",
"bestPerformance": "Mejor rendimiento",
"callQuality": "Calidad de vídeo",
"hd": "HD",
"hdTooltip": "Viendo vídeo en alta definición",
"highDefinition": "Alta definición",
"highestQuality": "Calidad máxima",
"labelTooiltipNoVideo": "Sin vídeo",
"labelTooltipAudioOnly": "Modo de ancho de banda bajo habilitado",
"ld": "LD",
"ldTooltip": "Viendo vídeo en baja definición",
"lowDefinition": "Baja definición",
"performanceSettings": "Ajustes de rendimiento",
"recording": "Grabación en curso",
"sd": "SD",
"sdTooltip": "Viendo vídeo en definición estándar",
"standardDefinition": "Definición estándar"
"standardDefinition": "Definición estándar",
"streaming": "Transmisión en curso"
},
"videothumbnail": {
"connectionInfo": "Información de conexión",
@@ -1090,12 +1366,19 @@
"domuteVideoOfOthers": "Desactivar la cámara de todos los demás",
"flip": "Voltear",
"grantModerator": "Convertir en moderador",
"hideSelfView": "Esconder vista propia",
"kick": "Expulsar",
"mirrorVideo": "Espejar mi video",
"moderator": "Moderador",
"mute": "Se silenció el participante",
"muted": "Silenciado",
"pinToStage": "",
"remoteControl": "Control remoto",
"screenSharing": "El participante está compartiendo su pantalla",
"show": "Mostrar en primer plano",
"showSelfView": "Mostrar vista propia",
"unpinFromStage": "",
"verify": "Verificar participante",
"videoMuted": "Cámara desactivada",
"videomute": "El participante paró su cámara"
},
@@ -1120,7 +1403,16 @@
"slightBlur": "Desenfoque Ligero",
"title": "Fondos virtuales",
"uploadedImage": "Imagen subida {{index}}",
"webAssemblyWarning": "No se admite WebAssembly"
"webAssemblyWarning": "No se admite WebAssembly",
"webAssemblyWarningDescription": "WebAssembly está desactivado o no cuenta con soporte en este navegador"
},
"visitors": {
"chatIndicator": "(visitante)",
"labelTooltip": "Cantidad de visitantes: {{count}}",
"notification": {
"description": "Levanta la mano para participar",
"title": "Eres un visitante en la reunión"
}
},
"volumeSlider": "Deslizador de volumen",
"welcomepage": {
@@ -1154,6 +1446,7 @@
"microsoftLogo": "Logotipo de Microsoft",
"policyLogo": "Logotipo de la política"
},
"meetingsAccessibilityLabel": "Reuniones",
"mobileDownLoadLinkAndroid": "Descargar la aplicación móvil para Android",
"mobileDownLoadLinkFDroid": "Descargar la aplicación móvil para F-Droid",
"mobileDownLoadLinkIos": "Descargar la aplicación móvil para iOS",
@@ -1162,13 +1455,21 @@
"recentList": "Reciente",
"recentListDelete": "Eliminar",
"recentListEmpty": "Tu historial de reuniones está vacío. Reúnete y aparecerán aquí.",
"recentMeetings": "Tus reuniones recientes",
"reducedUIText": "¡Bienvenido a {{app}}!",
"roomNameAllowedChars": "El nombre de la reunión no debe contener ninguno de estos caracteres: ?, &, :, ', \", %, #.",
"roomname": "Introduce el nombre de la sala",
"roomnameHint": "Introduce el nombre o URL de la sala a la que deseas unirte. Puedes inventar un nombre, simplemente infórmaselo a las personas con las que te reunirás para que introduzcan el mismo nombre.",
"sendFeedback": "Enviar sugerencias",
"settings": "Ajustes",
"startMeeting": "Iniciar la reunión",
"terms": "Términos",
"title": "Videoconferencias seguras, con gran variedad de funcionalidades y completamente gratuitas"
"title": "Videoconferencias seguras, con gran variedad de funcionalidades y completamente gratuitas",
"upcomingMeetings": "Tus próximas reuniones"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "Pizarra"
}
}
}

View File

@@ -1,5 +1,8 @@
{
"addPeople": {
"accessibilityLabel": {
"meetingLink": "Link da reunião: {{url}}"
},
"add": "Convidar",
"addContacts": "Convidar os seus contactos",
"contacts": "contactos",
@@ -39,6 +42,18 @@
"audioOnly": {
"audioOnly": "Largura de banda baixa"
},
"bandwidthSettings": {
"assumedBandwidthBps": "p. ex. 10000000 para 10 Mbps",
"assumedBandwidthBpsWarning": "Valores mais elevados podem causar problemas na rede.",
"customValue": "valor personalizado",
"customValueEffect": "para definir o valor actual de bps",
"leaveEmpty": "deixar em branco",
"leaveEmptyEffect": "para permitir a realização de estimativas",
"possibleValues": "Valores possíveis",
"setAssumedBandwidthBps": "Largura de banda presumida (bps)",
"title": "Definições de largura de banda",
"zeroEffect": "para desligar o vídeo"
},
"breakoutRooms": {
"actions": {
"add": "Adicionar salas simultâneas",
@@ -242,6 +257,8 @@
"WaitingForHostTitle": "À espera do anfitrião ...",
"Yes": "Sim",
"accessibilityLabel": {
"Cancel": "Cancelar (sair da caixa de diálogo)",
"Ok": "OK (guardar e sair da caixa de diálogo)",
"close": "Fechar caixa de diálogo",
"liveStreaming": "Transmissão em direto",
"sharingTabs": "Opções de partilha"
@@ -447,6 +464,9 @@
"title": "Incorporar esta reunião"
},
"feedback": {
"accessibilityLabel": {
"yourChoice": "A sua escolha: {{rating}}"
},
"average": "Média",
"bad": "Má",
"detailsLabel": "Conte-nos mais sobre isso.",
@@ -1151,6 +1171,7 @@
"muteEveryoneElse": "Silenciar todos os outros",
"muteEveryoneElsesVideo": "Parar o vídeo de todos os outros",
"muteEveryonesVideo": "Parar o vídeo de todos",
"muteGUMPending": "A ligar o seu microfone",
"noiseSuppression": "Supressão de ruído",
"openChat": "Abrir chat",
"participants": "Abrir painel de participantes",
@@ -1184,6 +1205,7 @@
"unmute": "Ligar microfone",
"videoblur": "Mudar o desfoque de vídeo",
"videomute": "Parar câmara",
"videomuteGUMPending": "A ligar a sua câmara",
"videounmute": "Iniciar câmara"
},
"addPeople": "Adicione pessoas à sua chamada",
@@ -1234,6 +1256,7 @@
"mute": "Desligar microfone",
"muteEveryone": "Silenciar todos",
"muteEveryonesVideo": "Desativar a câmara de todos",
"muteGUMPending": "A ligar o seu microfone",
"noAudioSignalDesc": "Se não o silenciou propositadamente a partir de configurações do sistema ou hardware, considere mudar de dispositivo.",
"noAudioSignalDescSuggestion": "Se não o silenciou propositadamente a partir das configurações do sistema ou hardware, considere mudar para o dispositivo sugerido.",
"noAudioSignalDialInDesc": "Também pode marcar usando:",
@@ -1279,6 +1302,7 @@
"unmute": "Ligar microfone",
"videoSettings": "Definições de vídeo",
"videomute": "Parar câmara",
"videomuteGUMPending": "A ligar a sua câmara",
"videounmute": "Iniciar câmara"
},
"transcribing": {
@@ -1325,7 +1349,7 @@
"audioOnly": "AUD",
"audioOnlyExpanded": "Está em modo de baixa largura de banda. Neste modo, receberá apenas partilha de áudio e ecrã.",
"bestPerformance": "Melhor desempenho",
"callQuality": "Qualidade de vídeo",
"callQuality": "Qualidade de vídeo (0 para o melhor desempenho, 3 para a melhor qualidade)",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
"highDefinition": "Alta definição (HD)",
@@ -1367,6 +1391,10 @@
"videomute": "Participante parou a câmara"
},
"virtualBackground": {
"accessibilityLabel": {
"currentBackground": "Atual imagem de fundo: {{background}}",
"selectBackground": "Selecionar uma imagem de fundo"
},
"addBackground": "Adicionar imagem de fundo",
"apply": "Aplicar",
"backgroundEffectError": "Falha ao aplicar efeito de fundo.",

View File

@@ -1,5 +1,8 @@
{
"addPeople": {
"accessibilityLabel": {
"meetingLink": "Meeting link: {{url}}"
},
"add": "Invite",
"addContacts": "Invite your contacts",
"contacts": "contacts",
@@ -254,6 +257,8 @@
"WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes",
"accessibilityLabel": {
"Cancel": "Cancel (leave dialog)",
"Ok": "OK (save and leave dialog)",
"close": "Close dialog",
"liveStreaming": "Live Stream",
"sharingTabs": "Sharing options"
@@ -459,6 +464,9 @@
"title": "Embed this meeting"
},
"feedback": {
"accessibilityLabel": {
"yourChoice": "Your choice: {{rating}}"
},
"average": "Average",
"bad": "Bad",
"detailsLabel": "Tell us more about it.",
@@ -1341,7 +1349,7 @@
"audioOnly": "AUD",
"audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.",
"bestPerformance": "Best performance",
"callQuality": "Video Quality",
"callQuality": "Video Quality (0 for best performance, 3 for highest quality)",
"hd": "HD",
"hdTooltip": "Viewing high definition video",
"highDefinition": "High definition",
@@ -1383,6 +1391,10 @@
"videomute": "Participant has stopped the camera"
},
"virtualBackground": {
"accessibilityLabel": {
"currentBackground": "Current background: {{background}}",
"selectBackground": "Select a background"
},
"addBackground": "Add background",
"apply": "Apply",
"backgroundEffectError": "Failed to apply background effect.",

View File

@@ -2027,6 +2027,23 @@ class API {
});
}
/**
* Notify external application (if API is enabled) if non participant message
* is received.
*
* @param {string} id - The resource id of the sender.
* @param {Object} json - The json carried by the message.
* @returns {void}
*/
notifyNonParticipantMessageReceived(id, json) {
this._sendEvent({
name: 'non-participant-message-received',
id,
message: json
});
}
/**
* Notify the external application (if API is enabled) if the connection type changed.
*

View File

@@ -128,6 +128,7 @@ const events = {
'mouse-enter': 'mouseEnter',
'mouse-leave': 'mouseLeave',
'mouse-move': 'mouseMove',
'non-participant-message-received': 'nonParticipantMessageReceived',
'notification-triggered': 'notificationTriggered',
'outgoing-message': 'outgoingMessage',
'p2p-status-changed': 'p2pStatusChanged',

View File

@@ -15,7 +15,7 @@ const Filmstrip = {
// horizontal film strip mode for calculating how tall large video
// display should be.
if (isFilmstripVisible(APP.store) && !interfaceConfig.VERTICAL_FILMSTRIP) {
return document.querySelector('.filmstrip').offsetHeight;
return document.querySelector('.filmstrip')?.offsetHeight ?? 0;
}
return 0;

184
package-lock.json generated
View File

@@ -17,7 +17,7 @@
"@giphy/react-components": "6.8.1",
"@giphy/react-native-sdk": "2.3.0",
"@hapi/bourne": "2.0.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.0.5",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
@@ -71,7 +71,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-emoji-render": "1.2.4",
"react-focus-lock": "2.9.4",
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.69.10",
@@ -3100,9 +3100,9 @@
}
},
"node_modules/@jitsi/excalidraw": {
"version": "0.0.13",
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
"integrity": "sha512-GcH+KwBTuE+3bdf73lS2X+TpVp/QFyXBHps8jntSWjz5UOfmXhF4SAoUe+550eVCHiZex78AaLaVflR34Lv0VA==",
"version": "0.0.14",
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"integrity": "sha512-iK7p7i6qJFOkjTVZhWDvurDW1u+eMoOhAVgpab9CZEqCTX+W4Ih4AOPrUpf+mjaAHK5XqmQZSc5nVEpMg+xIGQ==",
"license": "MIT",
"peerDependencies": {
"react": "^17.0.2 || ^18.2.0",
@@ -6926,6 +6926,17 @@
"sprintf-js": "~1.0.2"
}
},
"node_modules/aria-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -10640,6 +10651,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -15581,6 +15600,32 @@
}
}
},
"node_modules/react-focus-on": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
"integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
"dependencies": {
"aria-hidden": "^1.2.2",
"react-focus-lock": "^2.9.2",
"react-remove-scroll": "^2.5.6",
"react-style-singleton": "^2.2.0",
"tslib": "^2.3.1",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=8.5.0"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-freeze": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
@@ -16107,6 +16152,51 @@
"node": ">=0.10.0"
}
},
"node_modules/react-remove-scroll": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
"integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
"dependencies": {
"react-remove-scroll-bar": "^2.3.4",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
"dependencies": {
"react-style-singleton": "^2.2.1",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-shallow-renderer": {
"version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@@ -16119,6 +16209,28 @@
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"dependencies": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-textarea-autosize": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
@@ -21897,8 +22009,8 @@
"dev": true
},
"@jitsi/excalidraw": {
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
"integrity": "sha512-GcH+KwBTuE+3bdf73lS2X+TpVp/QFyXBHps8jntSWjz5UOfmXhF4SAoUe+550eVCHiZex78AaLaVflR34Lv0VA=="
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"integrity": "sha512-iK7p7i6qJFOkjTVZhWDvurDW1u+eMoOhAVgpab9CZEqCTX+W4Ih4AOPrUpf+mjaAHK5XqmQZSc5nVEpMg+xIGQ=="
},
"@jitsi/js-utils": {
"version": "2.0.5",
@@ -24738,6 +24850,14 @@
"sprintf-js": "~1.0.2"
}
},
"aria-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
"requires": {
"tslib": "^2.0.0"
}
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -27571,6 +27691,11 @@
"has-symbols": "^1.0.3"
}
},
"get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -31287,6 +31412,20 @@
"use-sidecar": "^1.1.2"
}
},
"react-focus-on": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
"integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
"requires": {
"aria-hidden": "^1.2.2",
"react-focus-lock": "^2.9.2",
"react-remove-scroll": "^2.5.6",
"react-style-singleton": "^2.2.0",
"tslib": "^2.3.1",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
}
},
"react-freeze": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
@@ -31658,6 +31797,27 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
"integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA=="
},
"react-remove-scroll": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
"integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
"requires": {
"react-remove-scroll-bar": "^2.3.4",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
}
},
"react-remove-scroll-bar": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
"requires": {
"react-style-singleton": "^2.2.1",
"tslib": "^2.0.0"
}
},
"react-shallow-renderer": {
"version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@@ -31667,6 +31827,16 @@
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
}
},
"react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"requires": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
}
},
"react-textarea-autosize": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",

View File

@@ -22,7 +22,7 @@
"@giphy/react-components": "6.8.1",
"@giphy/react-native-sdk": "2.3.0",
"@hapi/bourne": "2.0.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.0.5",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
@@ -76,7 +76,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-emoji-render": "1.2.4",
"react-focus-lock": "2.9.4",
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.69.10",

View File

@@ -268,6 +268,7 @@ class LoginDialog extends Component<IProps, IState> {
titleKey = { t('dialog.authenticationRequired') }>
<Input
autoFocus = { true }
id = 'login-dialog-username'
label = { t('dialog.user') }
name = 'username'
onChange = { this._onUsernameChange }
@@ -277,6 +278,7 @@ class LoginDialog extends Component<IProps, IState> {
<br />
<Input
className = 'dialog-bottom-margin'
id = 'login-dialog-password'
label = { t('dialog.userPassword') }
name = 'password'
onChange = { this._onPasswordChange }

View File

@@ -57,6 +57,14 @@ let mounted: boolean;
interface IProps {
/**
* The invisible text for screen readers.
*
* Intended to give the same info as `displayedText`, but can be customized to give more necessary context.
* If not given, `displayedText` will be used.
*/
accessibilityText?: string;
/**
* Css class to apply on container.
*/
@@ -93,7 +101,15 @@ interface IProps {
*
* @returns {React$Element<any>}
*/
function CopyButton({ className = '', displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: IProps) {
function CopyButton({
accessibilityText,
className = '',
displayedText,
textToCopy,
textOnHover,
textOnCopySuccess,
id
}: IProps) {
const { classes, cx } = useStyles();
const [ isClicked, setIsClicked ] = useState(false);
const [ isHovered, setIsHovered ] = useState(false);
@@ -196,20 +212,33 @@ function CopyButton({ className = '', displayedText, textToCopy, textOnHover, te
}
return (
<div
aria-label = { textOnHover }
className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
id = { id }
onBlur = { onHoverOut }
onClick = { onClick }
onFocus = { onHoverIn }
onKeyPress = { onKeyPress }
onMouseOut = { onHoverOut }
onMouseOver = { onHoverIn }
role = 'button'
tabIndex = { 0 }>
{ renderContent() }
</div>
<>
<div
aria-describedby = { displayedText === textOnHover
? undefined
: `${id}-sr-text` }
aria-label = { displayedText === textOnHover ? accessibilityText : textOnHover }
className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
id = { id }
onBlur = { onHoverOut }
onClick = { onClick }
onFocus = { onHoverIn }
onKeyPress = { onKeyPress }
onMouseOut = { onHoverOut }
onMouseOver = { onHoverIn }
role = 'button'
tabIndex = { 0 }>
{ renderContent() }
</div>
{ displayedText !== textOnHover && (
<span
className = 'sr-only'
id = { `${id}-sr-text` }>
{ accessibilityText }
</span>
)}
</>
);
}

View File

@@ -19,15 +19,68 @@ let screenLock: WakeLockSentinel | undefined;
/**
* Releases the screen lock.
*
* @returns {Promise}
*/
async function releaseScreenLock() {
if (screenLock) {
if (!screenLock.released) {
logger.debug('Releasing wake lock.');
try {
await screenLock.release();
} catch (e) {
logger.error(`Error while releasing the screen wake lock: ${e}.`);
}
}
screenLock.removeEventListener('release', onWakeLockReleased);
screenLock = undefined;
document.removeEventListener('visibilitychange', handleVisibilityChange);
}
}
/**
* Requests a new screen wake lock.
*
* @returns {void}
*/
function releaseScreenLock() {
if (screenLock) {
screenLock.release();
screenLock = undefined;
function requestWakeLock() {
if (navigator.wakeLock?.request) {
navigator.wakeLock.request('screen')
.then(lock => {
screenLock = lock;
screenLock.addEventListener('release', onWakeLockReleased);
document.addEventListener('visibilitychange', handleVisibilityChange);
logger.debug('Wake lock created.');
})
.catch(e => {
logger.error(`Error while requesting wake lock for screen: ${e}`);
});
}
}
/**
* Page visibility change handler that re-requests the wake lock if it has been released by the OS.
*
* @returns {void}
*/
async function handleVisibilityChange() {
if (screenLock?.released && document.visibilityState === 'visible') {
// The screen lock have been released by the OS because of document visibility change. Lets try to request the
// wake lock again.
await releaseScreenLock();
requestWakeLock();
}
}
/**
* Wake lock released handler.
*
* @returns {void}
*/
function onWakeLockReleased() {
logger.debug('Wake lock released');
}
MiddlewareRegistry.register(store => next => action => {
const { dispatch, getState } = store;
const { enableForcedReload } = getState()['features/base/config'];
@@ -43,15 +96,7 @@ MiddlewareRegistry.register(store => next => action => {
dispatch(setSkipPrejoinOnReload(false));
}
if (navigator.wakeLock?.request) {
navigator.wakeLock.request('screen')
.then(lock => {
screenLock = lock;
})
.catch(e => {
logger.error(`Error while requesting wake lock for screen: ${e}`);
});
}
requestWakeLock();
break;
}

View File

@@ -7,6 +7,16 @@ import { IIconProps } from './types';
interface IProps extends IIconProps {
/**
* Optional label for screen reader users.
*
* If set, this is will add a `aria-label` attribute on the svg element,
* contrary to the aria* props which set attributes on the container element.
*
* Use this if the icon conveys meaning and is not clickable.
*/
alt?: string;
/**
* The id of the element this button icon controls.
*/
@@ -114,6 +124,7 @@ export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
*/
export default function Icon(props: IProps) {
const {
alt,
className,
color,
id,
@@ -156,6 +167,13 @@ export default function Icon(props: IProps) {
const jitsiIconClassName = calculatedColor ? 'jitsi-icon' : 'jitsi-icon jitsi-icon-default';
const iconProps = alt ? {
'aria-label': alt,
role: 'img'
} : {
'aria-hidden': true
};
return (
<Container
{ ...rest }
@@ -176,6 +194,7 @@ export default function Icon(props: IProps) {
style = { restStyle }
tabIndex = { tabIndex }>
<IconComponent
{ ...iconProps }
fill = { calculatedColor }
height = { calculatedSize }
id = { id }

View File

@@ -7,6 +7,14 @@ import { COLORS } from '../../constants';
interface IProps {
/**
* Optional label for screen reader users, invisible in the UI.
*
* Note: if the text prop is set, a screen reader will first announce
* the accessibilityText, then the text.
*/
accessibilityText?: string;
/**
* Own CSS class name.
*/
@@ -82,6 +90,7 @@ const useStyles = makeStyles()(theme => {
});
const Label = ({
accessibilityText,
className,
color,
icon,
@@ -117,6 +126,7 @@ const Label = ({
color = { iconColor }
size = '16'
src = { icon } />}
{accessibilityText && <span className = 'sr-only'>{accessibilityText}</span>}
{text && <span className = { icon && classes.withIcon }>{text}</span>}
</div>
);

View File

@@ -3,7 +3,10 @@ import debounce from 'lodash/debounce';
import { IStore } from '../../app/types';
import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { SET_CAR_MODE } from '../../video-layout/actionTypes';
import {
SET_CAR_MODE,
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
} from '../../video-layout/actionTypes';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import { getParticipantById } from '../participants/functions';
@@ -81,6 +84,7 @@ MiddlewareRegistry.register(store => next => action => {
case SET_AUDIO_ONLY:
case SET_CAR_MODE:
case SET_FILMSTRIP_ENABLED:
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:
_updateLastN(store);
break;
}

View File

@@ -597,7 +597,8 @@ class VideoTransform extends Component<IProps, IState> {
this._onGesture('scale', scale);
}
} else if (gestureState.numberActiveTouches === 1
&& isNaN(this.initialDistance ?? 0)
&& (this.initialDistance === undefined
|| isNaN(this.initialDistance))
&& this._didMove(gestureState)) {
// this is a move event
const position = this._getTouchPosition(evt);

View File

@@ -152,7 +152,7 @@ class AudioTrack extends Component<IProps> {
const currentMuted = this._ref.muted;
const nextMuted = nextProps._muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
if (typeof nextMuted === 'boolean' && currentMuted !== nextMuted) {
this._ref.muted = nextMuted;
}
}

View File

@@ -1,5 +1,5 @@
import React, { Component, ReactNode } from 'react';
import ReactFocusLock from 'react-focus-lock';
import { FocusOn } from 'react-focus-on';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -40,6 +40,16 @@ interface IProps {
*/
disablePopover?: boolean;
/**
* Whether we can reach the popover element via keyboard or not when trigger is 'hover' (true by default).
*
* Only works when trigger is set to 'hover'.
*
* There are some rare cases where we want to set this to false,
* when the popover content is not necessary for screen reader users, because accessible elsewhere.
*/
focusable?: boolean;
/**
* The id of the dom element acting as the Popover label (matches aria-labelledby).
*/
@@ -103,6 +113,14 @@ interface IState {
position: string;
top?: string;
} | null;
/**
* Whether the popover should be focus locked or not.
*
* This is enabled if we notice the popover is interactive
* (trigger is click or focusable is true).
*/
enableFocusLock: boolean;
}
/**
@@ -119,6 +137,7 @@ class Popover extends Component<IProps, IState> {
*/
static defaultProps = {
className: '',
focusable: true,
id: '',
trigger: 'hover'
};
@@ -140,10 +159,12 @@ class Popover extends Component<IProps, IState> {
super(props);
this.state = {
contextMenuStyle: null
contextMenuStyle: null,
enableFocusLock: false
};
// Bind event handlers so they are only bound once for every instance.
this._enableFocusLock = this._enableFocusLock.bind(this);
this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
@@ -207,8 +228,8 @@ class Popover extends Component<IProps, IState> {
const { children,
className,
content,
focusable,
headingId,
headingLabel,
id,
overflowDrawer,
visible,
@@ -242,35 +263,40 @@ class Popover extends Component<IProps, IState> {
onKeyPress = { this._onKeyPress }
{ ...(trigger === 'hover' ? {
onMouseEnter: this._onShowDialog,
onMouseLeave: this._onHideDialog,
tabIndex: 0
onMouseLeave: this._onHideDialog
} : {}) }
{ ...(trigger === 'hover' && focusable && {
role: 'button',
tabIndex: 0
}) }
ref = { this._containerRef }>
{ visible && (
<DialogPortal
getRef = { this._setContextMenuRef }
onVisible = { this._isInteractive() ? this._enableFocusLock : undefined }
setSize = { this._setContextMenuStyle }
style = { this.state.contextMenuStyle }
targetSelector = '.popover-content'>
<ReactFocusLock
lockProps = {{
role: 'dialog',
'aria-modal': true,
'aria-labelledby': headingId,
'aria-label': !headingId && headingLabel ? headingLabel : undefined
}}
<FocusOn
// Use the `enabled` prop instead of conditionally rendering ReactFocusOn
// to prevent UI stutter on dialog appearance. It seems the focus guards generated annoy
// our DialogPortal positioning calculations.
enabled = { this.state.enableFocusLock }
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
// viewport on purpose (to be hidden). For example if we return the focus to the
// toolbox when it is hidden the whole page will move up in order to show the
// toolbox. This is usually followed up with displaying the toolbox (because now it
// is on focus) but because of the animation the whole scenario looks like jumping
// large video.
isElementInTheViewport
}>
}
shards = { [ this._contextMenuRef ] }>
{this._renderContent()}
</ReactFocusLock>
</FocusOn>
</DialogPortal>
)}
{ children }
@@ -361,12 +387,12 @@ class Popover extends Component<IProps, IState> {
* @returns {void}
*/
_onClick(event: React.MouseEvent) {
const { allowClick, trigger, visible } = this.props;
const { allowClick, trigger, focusable, visible } = this.props;
if (!allowClick) {
event.stopPropagation();
}
if (trigger === 'click') {
if (trigger === 'click' || focusable) {
if (visible) {
this._onHideDialog();
} else {
@@ -383,7 +409,9 @@ class Popover extends Component<IProps, IState> {
* @returns {void}
*/
_onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
// first check that the element we pressed is the actual popover toggle or any of its descendant,
// otherwise pressing space or enter in any child element of the popover _dialog_ will trigger this.
if (e.currentTarget.contains(e.target as Node) && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
if (this.props.visible) {
this._onHideDialog();
@@ -435,18 +463,49 @@ class Popover extends Component<IProps, IState> {
* @returns {ReactElement}
*/
_renderContent() {
const { content, position, trigger } = this.props;
const { content, position, trigger, headingId, headingLabel } = this.props;
return (
<div
className = { `popover ${trigger}` }
onKeyDown = { this._onEscKey }>
<div className = { `popover-content ${position.split('-')[0]}` }>
<div className = { `popover ${trigger}` }>
<div
className = { `popover-content ${position.split('-')[0]}` }
data-autofocus = { this.state.enableFocusLock }
onKeyDown = { this._onEscKey }
{ ...(this.state.enableFocusLock && {
'aria-modal': true,
'aria-label': !headingId && headingLabel ? headingLabel : undefined,
'aria-labelledby': headingId,
role: 'dialog',
tabIndex: -1
}) }>
{ content }
</div>
</div>
);
}
/**
* Returns whether the popover is considered interactive or not.
*
* Interactive means the popover content is certainly composed of buttons, links…
* Non-interactive popovers are mostly tooltips.
*
* @private
* @returns {boolean}
*/
_isInteractive() {
return this.props.trigger === 'click' || Boolean(this.props.focusable);
}
/**
* Enables the focus lock in the popover dialog.
*
* @private
* @returns {void}
*/
_enableFocusLock() {
this.setState({ enableFocusLock: true });
}
}
/**

View File

@@ -103,6 +103,7 @@ const BaseIndicator = ({
className = { className }
id = { id }>
<Icon
alt = { t(tooltipKey) }
className = { iconClassName }
color = { iconColor }
id = { iconId }

View File

@@ -24,6 +24,11 @@ interface IProps {
*/
footer?: any;
/**
* Id for the included input, necessary for screen readers.
*/
id: string;
/**
* Indicates if the component is disabled.
*/
@@ -174,6 +179,7 @@ class MultiSelectAutocomplete extends Component<IProps, IState> {
error = { this.state.error }
errorDialog = { errorDialog }
filterValue = { this.state.filterValue }
id = { this.props.id }
isOpen = { this.state.isOpen }
items = { this.state.items }
noMatchesText = { noMatchesFound }

View File

@@ -5,21 +5,6 @@ import Popover from '../../../popover/components/Popover.web';
interface IProps {
/**
* The id of the element this button icon controls.
*/
ariaControls?: string;
/**
* Whether the element popup is expanded.
*/
ariaExpanded?: boolean;
/**
* Whether the element has a popup.
*/
ariaHasPopup?: boolean;
/**
* Aria label for the Icon.
*/
@@ -40,11 +25,6 @@ interface IProps {
*/
iconDisabled?: boolean;
/**
* The ID of the icon button.
*/
iconId?: string;
/**
* Popover close callback.
*/
@@ -84,14 +64,10 @@ interface IProps {
*/
export default function ToolboxButtonWithPopup(props: IProps) {
const {
ariaControls,
ariaExpanded,
ariaHasPopup,
ariaLabel,
children,
icon,
iconDisabled,
iconId,
onPopoverClose,
onPopoverOpen,
popoverContent,
@@ -119,28 +95,6 @@ export default function ToolboxButtonWithPopup(props: IProps) {
);
}
const iconProps: {
ariaControls?: string;
ariaExpanded?: boolean;
className?: string;
containerId?: string;
role?: string;
tabIndex?: number;
} = {};
if (iconDisabled) {
iconProps.className
= 'settings-button-small-icon settings-button-small-icon--disabled';
} else {
iconProps.className = 'settings-button-small-icon';
iconProps.role = 'button';
iconProps.tabIndex = 0;
iconProps.ariaControls = ariaControls;
iconProps.ariaExpanded = ariaExpanded;
iconProps.containerId = iconId;
}
return (
<div
className = 'settings-button-container'
@@ -155,9 +109,10 @@ export default function ToolboxButtonWithPopup(props: IProps) {
position = 'top'
visible = { visible }>
<Icon
{ ...iconProps }
ariaHasPopup = { ariaHasPopup }
ariaLabel = { ariaLabel }
alt = { ariaLabel }
className = { `settings-button-small-icon ${iconDisabled
? 'settings-button-small-icon--disabled'
: ''}` }
size = { 16 }
src = { icon } />
</Popover>

View File

@@ -145,6 +145,7 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
allowClick = { true }
className = { containerClassName }
content = { contentComponent }
focusable = { false }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = { position }

View File

@@ -1,5 +1,5 @@
import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
import FocusLock from 'react-focus-lock';
import { FocusOn } from 'react-focus-on';
import { useTranslation } from 'react-i18next';
import { keyframes } from 'tss-react';
import { makeStyles } from 'tss-react/mui';
@@ -183,7 +183,7 @@ const BaseDialog = ({
<div
className = { classes.backdrop }
onClick = { onBackdropClick } />
<FocusLock
<FocusOn
className = { classes.focusLock }
returnFocus = {
@@ -196,14 +196,16 @@ const BaseDialog = ({
isElementInTheViewport
}>
<div
aria-describedby = { description }
aria-labelledby = { title ?? t(titleKey ?? '') }
aria-description = { description }
aria-label = { title ?? t(titleKey ?? '') }
aria-modal = { true }
className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
role = 'dialog'>
data-autofocus = { true }
role = 'dialog'
tabIndex = { -1 }>
{children}
</div>
</FocusLock>
</FocusOn>
</div>
);
};

View File

@@ -156,8 +156,8 @@ const Checkbox = ({
const isMobile = isMobileBrowser();
return (
<div className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
<label className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
<label className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
<div className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
<input
checked = { checked }
disabled = { disabled }
@@ -165,13 +165,14 @@ const Checkbox = ({
onChange = { onChange }
type = 'checkbox' />
<Icon
aria-hidden = { true }
className = 'checkmark'
color = { disabled ? theme.palette.icon03 : theme.palette.icon01 }
size = { 18 }
src = { IconCheck } />
</label>
<label>{label}</label>
</div>
</div>
<div>{label}</div>
</label>
);
};

View File

@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -76,6 +76,9 @@ export interface IProps {
/**
* You can use this item as a tab. Defaults to button if not set.
*
* If no onClick handler is provided, we assume the context menu item is
* not interactive and no role will be set.
*/
role?: 'tab' | 'button';
@@ -179,6 +182,28 @@ const ContextMenuItem = ({
const { classes: styles, cx } = useStyles();
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
const onKeyPressHandler = useCallback(e => {
// only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
onClick(e);
}
if (onKeyPress) {
onKeyPress(e);
}
}, [ onClick, onKeyPress, onKeyDown ]);
let tabIndex: undefined | 0 | -1;
if (role === 'tab') {
tabIndex = selected ? 0 : -1;
}
if (role === 'button' && !disabled) {
tabIndex = 0;
}
return (
<div
aria-controls = { controls }
@@ -196,12 +221,9 @@ const ContextMenuItem = ({
key = { text }
onClick = { disabled ? undefined : onClick }
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPress }
role = { role }
tabIndex = { role === 'tab'
? selected ? 0 : -1
: disabled ? undefined : 0
}>
onKeyPress = { disabled ? undefined : onKeyPressHandler }
role = { onClick ? role : undefined }
tabIndex = { onClick ? tabIndex : undefined }>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }

View File

@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
import { hideDialog } from '../../../dialog/actions';
import { IconCloseLarge } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { operatesWithEnterKey } from '../../functions.web';
import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
import Button from './Button';
@@ -108,8 +109,13 @@ const Dialog = ({
}, [ onCancel ]);
const submit = useCallback(() => {
!disableAutoHideOnSubmit && dispatch(hideDialog());
onSubmit?.();
if (onSubmit && (
(document.activeElement && !operatesWithEnterKey(document.activeElement))
|| !document.activeElement
)) {
!disableAutoHideOnSubmit && dispatch(hideDialog());
onSubmit();
}
}, [ onSubmit ]);
return (
@@ -124,11 +130,11 @@ const Dialog = ({
title = { title }
titleKey = { titleKey }>
<div className = { classes.header }>
<p
<h1
className = { classes.title }
id = 'dialog-title'>
{title ?? t(titleKey ?? '')}
</p>
</h1>
{!hideCloseButton && (
<ClickableIcon
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
@@ -160,6 +166,7 @@ const Dialog = ({
accessibilityLabel = { t(ok.translationKey ?? '') }
disabled = { ok.disabled }
id = 'modal-dialog-ok-button'
isSubmit = { true }
labelKey = { ok.translationKey }
onClick = { submit } />}
</div>

View File

@@ -1,5 +1,4 @@
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
import { MoveFocusInside } from 'react-focus-lock';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -187,7 +186,7 @@ const DialogWithTabs = ({
useEffect(() => {
if (isMobile) {
setSelectedTab(undefined);
setSelectedTab(defaultTab);
} else {
setSelectedTab(defaultTab ?? tabs[0].name);
}
@@ -317,20 +316,19 @@ const DialogWithTabs = ({
<BaseDialog
className = { cx(classes.dialog, className) }
onClose = { onClose }
size = 'large'>
size = 'large'
titleKey = { titleKey }>
{(!isMobile || !selectedTab) && (
<div
aria-orientation = 'vertical'
className = { classes.sidebar }
role = { isMobile ? undefined : 'tablist' }>
<div className = { classes.titleContainer }>
<MoveFocusInside>
<h2
className = { classes.title }
tabIndex = { -1 }>
{t(titleKey ?? '')}
</h2>
</MoveFocusInside>
<h1
className = { classes.title }
tabIndex = { -1 }>
{t(titleKey ?? '')}
</h1>
{isMobile && closeIcon}
</div>
{tabs.map((tab, index) => {
@@ -366,11 +364,11 @@ const DialogWithTabs = ({
{isMobile && (
<div className = { cx(classes.buttonContainer, classes.header) }>
<span className = { classes.backContainer }>
<h2
<h1
className = { classes.title }
tabIndex = { -1 }>
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
</h2>
</h1>
<ClickableIcon
accessibilityLabel = { t('dialog.Back') }
icon = { IconArrowBack }
@@ -401,13 +399,13 @@ const DialogWithTabs = ({
<div
className = { cx(classes.buttonContainer, classes.footer) }>
<Button
accessibilityLabel = { t('dialog.Cancel') }
accessibilityLabel = { t('dialog.accessibilityLabel.Cancel') }
id = 'modal-dialog-cancel-button'
labelKey = { 'dialog.Cancel' }
onClick = { onClose }
type = 'tertiary' />
<Button
accessibilityLabel = { t('dialog.Ok') }
accessibilityLabel = { t('dialog.accessibilityLabel.Ok') }
id = 'modal-dialog-ok-button'
labelKey = { 'dialog.Ok' }
onClick = { onSubmit } />

View File

@@ -15,12 +15,19 @@ interface IProps extends IInputProps {
bottomLabel?: string;
className?: string;
iconClick?: () => void;
id?: string;
/**
* The id to set on the input element.
* This is required because we need it internally to tie the input to its
* info (label, error) so that screen reader users don't get lost.
*/
id: string;
maxLength?: number;
maxRows?: number;
maxValue?: number;
minRows?: number;
minValue?: number;
mode?: 'text' | 'none' | 'decimal' | 'numeric' | 'tel' | 'search' | ' email' | 'url';
name?: string;
onBlur?: (e: any) => void;
onFocus?: (event: React.FocusEvent) => void;
@@ -162,6 +169,7 @@ const Input = React.forwardRef<any, IProps>(({
maxRows,
minValue,
minRows,
mode,
name,
onBlur,
onChange,
@@ -185,7 +193,11 @@ const Input = React.forwardRef<any, IProps>(({
return (
<div className = { cx(styles.inputContainer, className) }>
{label && <span className = { cx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
{label && <label
className = { cx(styles.label, isMobile && 'is-mobile') }
htmlFor = { id } >
{label}
</label>}
<div className = { styles.fieldContainer }>
{icon && <Icon
{ ...(iconClick ? { tabIndex: 0 } : {}) }
@@ -201,7 +213,7 @@ const Input = React.forwardRef<any, IProps>(({
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
disabled = { disabled }
{ ...(id ? { id } : {}) }
id = { id }
maxLength = { maxLength }
maxRows = { maxRows }
minRows = { minRows }
@@ -215,6 +227,7 @@ const Input = React.forwardRef<any, IProps>(({
value = { value } />
) : (
<input
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
@@ -222,7 +235,8 @@ const Input = React.forwardRef<any, IProps>(({
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
data-testid = { testId }
disabled = { disabled }
{ ...(id ? { id } : {}) }
id = { id }
{ ...(mode ? { inputmode: mode } : {}) }
{ ...(type === 'number' ? { max: maxValue } : {}) }
maxLength = { maxLength }
{ ...(type === 'number' ? { min: minValue } : {}) }
@@ -246,7 +260,9 @@ const Input = React.forwardRef<any, IProps>(({
</button>}
</div>
{bottomLabel && (
<span className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
<span
className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }
id = { `${id}-description` }>
{bottomLabel}
</span>
)}

View File

@@ -14,6 +14,7 @@ interface IProps {
error?: boolean;
errorDialog?: JSX.Element | null;
filterValue?: string;
id: string;
isOpen?: boolean;
items: MultiSelectItem[];
noMatchesText?: string;
@@ -101,6 +102,7 @@ const MultiSelect = ({
error,
errorDialog,
placeholder,
id,
items,
filterValue,
onFilterChange,
@@ -145,6 +147,7 @@ const MultiSelect = ({
<Input
autoFocus = { autoFocus }
disabled = { disabled }
id = { id }
onChange = { onFilterChange }
placeholder = { placeholder }
ref = { inputRef }

View File

@@ -28,6 +28,12 @@ interface ISelectProps {
*/
error?: boolean;
/**
* Id of the <select> element.
* Necessary for screen reader users, to link the label and error to the select.
*/
id: string;
/**
* Label to be displayed above the select.
*/
@@ -140,6 +146,7 @@ const Select = ({
className,
disabled,
error,
id,
label,
onChange,
options,
@@ -149,11 +156,17 @@ const Select = ({
return (
<div className = { classes.container }>
{label && <span className = { cx(classes.label, isMobile && 'is-mobile') }>{label}</span>}
{label && <label
className = { cx(classes.label, isMobile && 'is-mobile') }
htmlFor = { id } >
{label}
</label>}
<div className = { classes.selectContainer }>
<select
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') }
disabled = { disabled }
id = { id }
onChange = { onChange }
value = { value }>
{options.map(option => (<option
@@ -167,7 +180,9 @@ const Select = ({
src = { IconArrowDown } />
</div>
{bottomLabel && (
<span className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
<span
className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }
id = { `${id}-description` }>
{bottomLabel}
</span>
)}

View File

@@ -52,6 +52,7 @@ const useStyles = makeStyles()(theme => {
width: '16px',
height: '16px',
position: 'absolute',
zIndex: 5,
top: '4px',
left: '4px',
backgroundColor: theme.palette.ui10,
@@ -73,8 +74,38 @@ const useStyles = makeStyles()(theme => {
},
checkbox: {
height: 0,
width: 0
position: 'absolute',
zIndex: 10,
cursor: 'pointer',
left: 0,
right: 0,
top: 0,
bottom: 0,
width: '100%',
height: '100%',
opacity: 0,
'&.focus-visible + .toggle-checkbox-ring': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
}
},
checkboxRing: {
position: 'absolute',
pointerEvents: 'none',
zIndex: 6,
left: 0,
right: 0,
top: 0,
bottom: 0,
width: '100%',
height: '100%',
borderRadius: '12px',
'&.is-mobile': {
borderRadius: '32px'
}
}
};
});
@@ -88,7 +119,7 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
}, []);
return (
<label
<span
className = { cx('toggle-container', styles.container, checked && styles.containerOn,
isMobile && 'is-mobile', disabled && 'disabled', className) }>
<input
@@ -98,8 +129,9 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
className = { styles.checkbox }
disabled = { disabled }
onChange = { change } />
<div className = { cx('toggle-checkbox-ring', styles.checkboxRing, isMobile && 'is-mobile') } />
<div className = { cx('toggle', styles.toggle, checked && styles.toggleOn, isMobile && 'is-mobile') } />
</label>
</span>
);
};

View File

@@ -82,3 +82,28 @@ export function isElementInTheViewport(element?: Element): boolean {
return false;
}
const enterKeyElements = [ 'select', 'textarea', 'summary', 'a' ];
/**
* Informs whether or not the given element does something on its own when pressing the Enter key.
*
* This is useful to correctly submit custom made "forms" that are not using the native form element,
* only when the user is not using an element that needs the enter key to work.
* Note the implementation is incomplete and should be updated as needed if more complex use cases arise
* (for example, the Tabs aria pattern is not handled).
*
* @param {Element} element - The element.
* @returns {boolean}
*/
export function operatesWithEnterKey(element: Element): boolean {
if (enterKeyElements.includes(element.tagName.toLowerCase())) {
return true;
}
if (element.tagName.toLowerCase() === 'button' && element.getAttribute('role') === 'button') {
return true;
}
return false;
}

View File

@@ -25,6 +25,10 @@ export function parseURLParams(
url: URL | string,
dontParse = false,
source = 'hash') {
if (!url) {
return {};
}
if (typeof url === 'string') {
// eslint-disable-next-line no-param-reassign
url = new URL(url);

View File

@@ -135,6 +135,7 @@ class ChatInput extends Component<IProps, IState> {
className = 'chat-input'
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
iconClick = { this._toggleSmileysPanel }
id = 'chat-input-messagebox'
maxRows = { 5 }
onChange = { this._onMessageChange }
onKeyPress = { this._onDetectSubmit }

View File

@@ -32,6 +32,7 @@ const RaisedHandsCountLabel = () => {
content = { t('raisedHandsLabel') }
position = { 'bottom' }>
<Label
accessibilityText = { t('raisedHandsLabel') }
className = { styles.label }
icon = { IconRaiseHand }
iconColor = { theme.palette.icon04 }

View File

@@ -176,7 +176,7 @@ function _conferenceJoined({ dispatch, getState }: IStore) {
function _checkIframe(state: IReduxState, dispatch: IStore['dispatch']) {
let allowIframe = false;
if (document.referrer === '') {
if (document.referrer === '' && browser.isElectron()) {
// no iframe
allowIframe = true;
} else {

View File

@@ -347,12 +347,14 @@ class ConnectionIndicator extends AbstractConnectionIndicator<IProps, IState> {
_connectionIndicatorInactiveDisabled,
_videoTrack,
classes,
iconSize
iconSize,
t
} = this.props;
return (
<div
style = {{ fontSize: iconSize }}>
<span className = 'sr-only'>{ t('videothumbnail.connectionInfo') }</span>
<ConnectionIndicatorIcon
classes = { classes }
colorClass = { this._getConnectionColorClass() }

View File

@@ -259,6 +259,11 @@ const useStyles = makeStyles()(theme => {
cursor: 'pointer',
color: theme.palette.link01,
transition: 'color .2s ease',
border: 0,
background: 0,
padding: 0,
display: 'inline',
fontWeight: 'bold',
'&:hover': {
color: theme.palette.link01Hover,
@@ -714,13 +719,12 @@ const ConnectionStatsTable = ({
const _renderSaveLogs = () => (
<span>
<a
<button
className = { cx(classes.link, 'savelogs') }
onClick = { onSaveLogs }
role = 'button'
tabIndex = { 0 }>
type = 'button'>
{t('connectionindicator.savelogs')}
</a>
</button>
<span> | </span>
</span>
);
@@ -732,13 +736,12 @@ const ConnectionStatsTable = ({
: 'connectionindicator.more';
return (
<a
<button
className = { cx(classes.link, 'showmore') }
onClick = { onShowMore }
role = 'button'
tabIndex = { 0 }>
type = 'button'>
{t(translationKey)}
</a>
</button>
);
};

View File

@@ -69,6 +69,7 @@ const useStyles = makeStyles()(theme => {
const DeviceSelector = ({
devices,
hasPermission,
id,
isDisabled,
label,
onSelect,
@@ -103,6 +104,7 @@ const DeviceSelector = ({
return (
<Select
id = { id }
label = { t(label) }
onChange = { _onSelect }
options = { options.items }

View File

@@ -351,6 +351,7 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
? t('settings.desktopShareHighFpsWarning')
: t('settings.desktopShareWarning') }
id = 'more-framerate-select'
label = { t('settings.desktopShareFramerate') }
onChange = { this._onFramerateItemSelect }
options = { frameRateItems }

View File

@@ -58,6 +58,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'dialog-displayName'
label = { this.props.t('dialog.enterDisplayName') }
name = 'displayName'
onChange = { this._onDisplayNameChange }

View File

@@ -55,13 +55,15 @@ function EmbedMeeting({ t, url }: IProps) {
<div className = { classes.container }>
<Input
accessibilityLabel = { t('dialog.embedMeeting') }
id = 'embed-meeting-input'
readOnly = { true }
textarea = { true }
value = { getEmbedCode() } />
<CopyButton
aria-label = { t('addPeople.copyLink') }
accessibilityText = { t('addPeople.copyLink') }
className = { classes.button }
displayedText = { t('dialog.copy') }
id = 'embed-meeting-copy-button'
textOnCopySuccess = { t('dialog.copied') }
textOnHover = { t('dialog.copy') }
textToCopy = { getEmbedCode() } />

View File

@@ -25,7 +25,7 @@ const styles = (theme: Theme) => {
rating: {
display: 'flex',
flexDirection: 'column' as const,
flexDirection: 'column-reverse' as const,
alignItems: 'center',
justifyContent: 'center',
marginTop: theme.spacing(4),
@@ -316,22 +316,27 @@ class FeedbackDialog extends Component<IProps, IState> {
titleKey = 'feedback.rateExperience'>
<div className = { classes.dialog }>
<div className = { classes.rating }>
<div
aria-label = { this.props.t('feedback.star') }
className = { classes.ratingLabel } >
<p id = 'starLabel'>
{ t(SCORES[scoreToDisplayAsSelected]) }
</p>
</div>
<div
className = { classes.stars }
onMouseLeave = { this._onScoreContainerMouseLeave }>
{ scoreIcons }
</div>
<div
className = { classes.ratingLabel } >
<p className = 'sr-only'>
{ t('feedback.accessibilityLabel.yourChoice', {
rating: t(SCORES[scoreToDisplayAsSelected])
}) }
</p>
<p
aria-hidden = { true }
id = 'starLabel'>
{ t(SCORES[scoreToDisplayAsSelected]) }
</p>
</div>
</div>
<div className = { classes.details }>
<Input
autoFocus = { true }
id = 'feedbackTextArea'
label = { t('feedback.detailsLabel') }
onChange = { this._onMessageChange }

View File

@@ -903,6 +903,7 @@ class Thumbnail extends Component<IProps, IState> {
tabIndex = { 0 }>
{avatarURL ? (
<img
alt = ''
className = 'sharedVideoAvatar'
src = { avatarURL } />
)
@@ -1105,6 +1106,20 @@ class Thumbnail extends Component<IProps, IState> {
? <span id = 'localVideoWrapper'>{video}</span>
: video)}
<div className = { classes.containerBackground } />
{/* put the bottom container before the top container in the dom,
because it contains the participant name that should be announced first by screen readers */}
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
local = { local }
participantId = { id }
showStatusIndicators = { !isWhiteboardParticipant(_participant) }
thumbnailType = { _thumbnailType } />
</div>
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsTopContainer,
@@ -1122,18 +1137,6 @@ class Thumbnail extends Component<IProps, IState> {
thumbnailType = { _thumbnailType } />
</div>
{_shouldDisplayTintBackground && <div className = { classes.tintBackground } />}
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
local = { local }
participantId = { id }
showStatusIndicators = { !isWhiteboardParticipant(_participant) }
thumbnailType = { _thumbnailType } />
</div>
{!_gifSrc && this._renderAvatar(styles.avatar) }
{ !local && (
<div className = 'presence-label-container'>

View File

@@ -208,6 +208,7 @@ function GifsMenu({ columns = 2, parent }: IProps) {
<Input
autoFocus = { true }
className = { cx(styles.searchField, 'gif-input') }
id = 'gif-search-input'
onChange = { handleSearchKeyChange }
onKeyPress = { onInputKeyPress }
placeholder = { t('giphy.search') }

View File

@@ -34,15 +34,12 @@ function CopyMeetingLinkSection({ url }: IProps) {
return (
<>
<label
className = { classes.label }
htmlFor = { 'copy-button-id' }
id = 'copy-button-label'>{t('addPeople.shareLink')}</label>
<p className = { classes.label }>{t('addPeople.shareLink')}</p>
<CopyButton
aria-label = { t('addPeople.copyLink') }
accessibilityText = { t('addPeople.accessibilityLabel.meetingLink', { url: getDecodedURI(url) }) }
className = 'invite-more-dialog-conference-url'
displayedText = { getDecodedURI(url) }
id = 'copy-button-id'
id = 'add-people-copy-link-button'
textOnCopySuccess = { t('addPeople.linkCopied') }
textOnHover = { t('addPeople.copyLink') }
textToCopy = { url } />

View File

@@ -44,7 +44,6 @@ class DialInNumber extends Component<IProps> {
// Bind event handler so it is only bound once for every instance.
this._onCopyText = this._onCopyText.bind(this);
this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
}
/**
@@ -62,20 +61,6 @@ class DialInNumber extends Component<IProps> {
copyText(textToCopy);
}
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onCopyTextKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onCopyText();
}
}
/**
* Implements React's {@link Component#render()}.
*
@@ -87,7 +72,7 @@ class DialInNumber extends Component<IProps> {
return (
<div className = 'dial-in-number'>
<div>
<p>
<span className = 'phone-number'>
<span className = 'info-label'>
{ t('info.dialInNumber') }
@@ -107,16 +92,13 @@ class DialInNumber extends Component<IProps> {
{ `${_formatConferenceIDPin(conferenceID)}#` }
</span>
</span>
</div>
<a
</p>
<button
aria-label = { t('info.copyNumber') }
className = 'dial-in-copy'
onClick = { this._onCopyText }
onKeyPress = { this._onCopyTextKeyPress }
role = 'button'
tabIndex = { 0 }>
className = 'dial-in-copy invisible-button'
onClick = { this._onCopyText }>
<Icon src = { IconCopy } />
</a>
</button>
</div>
);
}

View File

@@ -185,6 +185,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
className = { this.props.classes.formWrap }
onKeyDown = { this._onKeyDown }>
<MultiSelectAutocomplete
id = 'invite-contacts-input'
isDisabled = { isMultiSelectDisabled }
loadingMessage = { t(loadingMessage) }
noMatchesFound = { t(noMatches) }

View File

@@ -158,6 +158,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
return (
<Input
className = 'lobby-prejoin-input'
id = 'lobby-name-field'
onChange = { this._onChangeDisplayName }
placeholder = { t('lobby.nameField') }
testId = 'lobby.nameField'
@@ -177,6 +178,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
<>
<Input
className = { `lobby-prejoin-input ${_passwordJoinFailed ? 'error' : ''}` }
id = 'lobby-password-input'
onChange = { this._onChangePassword }
placeholder = { t('lobby.passwordField') }
testId = 'lobby.password'

View File

@@ -127,6 +127,7 @@ function MeetingParticipants({
<Input
className = { styles.search }
clearable = { true }
id = 'participants-search-input'
onChange = { setSearchString }
placeholder = { t('participantsPane.search') }
value = { searchString } />

View File

@@ -191,6 +191,7 @@ const PollCreate = ({
<div className = { classes.questionContainer }>
<Input
autoFocus = { true }
id = 'polls-create-input'
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { setQuestion }
@@ -205,6 +206,7 @@ const PollCreate = ({
className = { classes.answer }
key = { i }>
<Input
id = { `polls-answer-input-${i}` }
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }

View File

@@ -374,10 +374,12 @@ const Prejoin = ({
className = { classes.inputContainer }
data-testid = 'prejoin.screen'>
{showDisplayNameField.current ? (<Input
accessibilityLabel = { t('dialog.enterDisplayName') }
autoComplete = { 'name' }
autoFocus = { true }
className = { classes.input }
error = { showErrorOnJoin }
id = 'premeeting-name-input'
onChange = { setName }
onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
placeholder = { t('dialog.enterDisplayName') }

View File

@@ -119,9 +119,6 @@ function ReactionsMenuButton({
if (_reactionsButtonEnabled) {
content = (
<ToolboxButtonWithPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
onPopoverClose = { closeReactionsMenu }
onPopoverOpen = { openReactionsMenu }
@@ -141,13 +138,9 @@ function ReactionsMenuButton({
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
onPopoverClose = { toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }

View File

@@ -38,7 +38,8 @@ export interface IProps extends AbstractButtonProps {
* An abstract class of a button for starting and stopping live streaming.
*/
export default class AbstractLiveStreamButton<P extends IProps> extends AbstractButton<P> {
accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming';
accessibilityLabel = 'dialog.startLiveStreaming';
toggledAccessibilityLabel = 'dialog.stopLiveStreaming';
icon = IconSites;
label = 'dialog.startLiveStreaming';
toggledLabel = 'dialog.stopLiveStreaming';

View File

@@ -39,20 +39,6 @@ const styles = (theme: Theme) => {
*/
class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
/**
* Initializes a new {@code StreamKeyForm} instance.
*
* @param {IProps} props - The React {@code Component} props to initialize
* the new {@code StreamKeyForm} instance with.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onOpenHelp = this._onOpenHelp.bind(this);
this._onOpenHelpKeyPress = this._onOpenHelpKeyPress.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@@ -66,6 +52,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
<div className = 'stream-key-form'>
<Input
autoFocus = { true }
id = 'streamkey-input'
label = { t('dialog.streamKey') }
name = 'streamId'
onChange = { this._onInputChange }
@@ -83,12 +70,10 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
}
{ this.props._liveStreaming.helpURL
? <a
aria-label = { t('liveStreaming.streamIdHelp') }
className = { classes.helperLink }
onClick = { this._onOpenHelp }
onKeyPress = { this._onOpenHelpKeyPress }
role = 'link'
tabIndex = { 0 }>
href = { this.props._liveStreaming.helpURL }
rel = 'noopener noreferrer'
target = '_blank'>
{ t('liveStreaming.streamIdHelp') }
</a>
: null
@@ -112,33 +97,6 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
</div>
);
}
/**
* Opens a new tab with information on how to manually locate a YouTube
* broadcast stream key.
*
* @private
* @returns {void}
*/
_onOpenHelp() {
window.open(this.props._liveStreaming.helpURL, '_blank', 'noopener');
}
/**
* Opens a new tab with information on how to manually locate a YouTube
* broadcast stream key.
*
* @param {Object} e - The key event to handle.
*
* @private
* @returns {void}
*/
_onOpenHelpKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ') {
e.preventDefault();
this._onOpenHelp();
}
}
}
export default translate(connect(_mapStateToProps)(withStyles(styles)(StreamKeyForm)));

View File

@@ -100,6 +100,7 @@ class StreamKeyPicker extends PureComponent<IProps> {
return (
<div className = 'broadcast-dropdown dropdown-menu'>
<Select
id = 'streamkeypicker-select'
label = { t('liveStreaming.choose') }
onChange = { this._onSelect }
options = { dropdownItems }

View File

@@ -36,7 +36,8 @@ export interface IProps extends AbstractButtonProps {
* An abstract implementation of a button for starting and stopping recording.
*/
export default class AbstractRecordButton<P extends IProps> extends AbstractButton<P> {
accessibilityLabel = 'toolbar.accessibilityLabel.recording';
accessibilityLabel = 'dialog.startRecording';
toggledAccessibilityLabel = 'dialog.stopRecording';
icon = IconRecord;
label = 'dialog.startRecording';
toggledLabel = 'dialog.stopRecording';

View File

@@ -70,6 +70,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
},
get mediaType() {
if (this.selfRecording.on && !this.selfRecording.withVideo) {
return 'audio/webm;';
}
if (!preferredMediaType) {
preferredMediaType = getMimeType();
}

View File

@@ -79,6 +79,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
checked = { selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE }
className = 'recording-switch'
disabled = { isValidating }
id = 'recording-switch-jitsi'
onChange = { this._onRecordingServiceSwitchChange } />
) : null;
@@ -98,12 +99,15 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
key = 'noIntegrationSetting'>
<Container className = { contentRecordingClass }>
<Image
alt = ''
className = 'content-recording-icon'
src = { ICON_CLOUD } />
</Container>
<Text className = 'recording-title'>
<label
className = 'recording-title'
htmlFor = 'recording-switch-jitsi'>
{ label }
</Text>
</label>
{ switchContent }
</Container>
);
@@ -132,16 +136,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
key = 'fileSharingSetting'>
<Container className = 'recording-icon-container file-sharing-icon-container'>
<Image
alt = ''
className = 'recording-file-sharing-icon'
src = { ICON_USERS } />
</Container>
<Text className = 'recording-title'>
<label
className = 'recording-title'
htmlFor = 'recording-switch-share'>
{ t('recording.fileSharingdescription') }
</Text>
</label>
<Switch
checked = { sharingSetting }
className = 'recording-switch'
disabled = { isValidating }
id = 'recording-switch-share'
onChange = { onSharingSettingChanged } />
</Container>
);
@@ -169,6 +177,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
className = 'recording-info'
key = 'cloudUploadInfo'>
<Image
alt = ''
className = 'recording-info-icon'
src = { ICON_INFO } />
<Text className = 'recording-info-title'>
@@ -246,6 +255,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
} = this.props;
let content = null;
let switchContent = null;
let labelContent = (
<Text className = 'recording-title'>
{ t('recording.authDropboxText') }
</Text>
);
if (isValidating) {
content = this._renderSpinner();
@@ -281,8 +295,16 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
=== RECORDING_TYPES.DROPBOX }
className = 'recording-switch'
disabled = { isValidating }
id = 'recording-switch-integration'
onChange = { this._onDropboxSwitchChange } />
);
labelContent = (
<label
className = 'recording-title'
htmlFor = 'recording-switch-integration'>
{ t('recording.authDropboxText') }
</label>
);
}
return (
@@ -293,12 +315,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
<Container
className = 'recording-icon-container'>
<Image
alt = ''
className = 'recording-icon'
src = { DROPBOX_LOGO } />
</Container>
<Text className = 'recording-title'>
{ t('recording.authDropboxText') }
</Text>
{ labelContent }
{ switchContent }
</Container>
<Container className = 'authorization-panel'>
@@ -338,17 +359,21 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
<Container
className = 'recording-icon-container'>
<Image
alt = ''
className = 'recording-icon'
src = { LOCAL_RECORDING } />
</Container>
<Text className = 'recording-title'>
<label
className = 'recording-title'
htmlFor = 'recording-switch-local'>
{ t('recording.saveLocalRecording') }
</Text>
</label>
<Switch
checked = { selectedRecordingService
=== RECORDING_TYPES.LOCAL }
className = 'recording-switch'
disabled = { isValidating }
id = 'recording-switch-local'
onChange = { this._onLocalRecordingSwitchChange } />
</Container>
</Container>
@@ -359,16 +384,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
<Container className = 'recording-header space-top'>
<Container className = 'recording-icon-container file-sharing-icon-container'>
<Image
alt = ''
className = 'recording-file-sharing-icon'
src = { ICON_USERS } />
</Container>
<Text className = 'recording-title'>
<label
className = 'recording-title'
htmlFor = 'recording-switch-myself'>
{t('recording.onlyRecordSelf')}
</Text>
</label>
<Switch
checked = { Boolean(localRecordingOnlySelf) }
className = 'recording-switch'
disabled = { isValidating }
id = 'recording-switch-myself'
onChange = { onLocalRecordingSelfChange ?? EMPTY_FUNCTION } />
</Container>
</Container>

View File

@@ -93,6 +93,7 @@ class PasswordRequiredPrompt extends Component<IProps, IState> {
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'required-password-input'
label = { this.props.t('dialog.passwordLabel') }
name = 'lockKey'
onChange = { this._onPasswordChanged }

View File

@@ -83,6 +83,7 @@ class ShareAudioDialog extends Component<IProps> {
titleKey = { t('dialog.shareAudioTitle') }>
<div className = 'share-audio-dialog'>
<img
alt = ''
className = 'share-audio-animation'
src = 'images/share-audio.gif' />
<Checkbox

View File

@@ -140,22 +140,29 @@ class PasswordForm extends Component<IProps, IState> {
* @returns {ReactElement}
*/
_renderPasswordField() {
if (this.props.editEnabled) {
let placeHolderText = this.props.t('dialog.password');
const {
editEnabled,
passwordNumberOfDigits,
t
} = this.props;
if (this.props.passwordNumberOfDigits) {
if (editEnabled) {
let placeHolderText = t('dialog.password');
if (passwordNumberOfDigits) {
placeHolderText = this.props.t('passwordDigitsOnly', {
number: this.props.passwordNumberOfDigits });
number: passwordNumberOfDigits });
}
return (
<div
className = 'info-password-form'>
<Input
accessibilityLabel = { this.props.t('info.addPassword') }
accessibilityLabel = { t('info.addPassword') }
autoFocus = { true }
id = 'info-password-input'
maxLength = { this.props.passwordNumberOfDigits }
maxLength = { passwordNumberOfDigits }
mode = { passwordNumberOfDigits ? 'numeric' : undefined }
onChange = { this._onEnteredPasswordChange }
onKeyPress = { this._onKeyPress }
placeholder = { placeHolderText }

View File

@@ -168,67 +168,6 @@ function PasswordSection({
copyText(password ?? '');
}
/**
* Toggles whether or not the password should currently be shown as being
* edited locally.
*
* @param {Object} e - The key event to handle.
*
* @private
* @returns {void}
*/
function onTogglePasswordEditStateKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onTogglePasswordEditState();
}
}
/**
* Method to remotely submit the password from outside of the password form.
*
* @param {Object} e - The key event to handle.
*
* @private
* @returns {void}
*/
function onPasswordSaveKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onPasswordSave();
}
}
/**
* Callback invoked to unlock the current JitsiConference.
*
* @param {Object} e - The key event to handle.
*
* @private
* @returns {void}
*/
function onPasswordRemoveKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onPasswordRemove();
}
}
/**
* Copies the password to the clipboard.
*
* @param {Object} e - The key event to handle.
*
* @private
* @returns {void}
*/
function onPasswordCopyKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onPasswordCopy();
}
}
/**
* Callback invoked to show the current password.
*
@@ -238,20 +177,6 @@ function PasswordSection({
setPasswordVisible(true);
}
/**
* Callback invoked to show the current password.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
function onPasswordShowKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
setPasswordVisible(true);
}
}
/**
* Callback invoked to hide the current password.
*
@@ -261,20 +186,6 @@ function PasswordSection({
setPasswordVisible(false);
}
/**
* Callback invoked to hide the current password.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
function onPasswordHideKeyPressHandler(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
setPasswordVisible(false);
}
}
/**
* Method that renders the password action(s) based on the current
* locked-status of the conference.
@@ -289,18 +200,20 @@ function PasswordSection({
if (passwordEditEnabled) {
return (
<>
<a
aria-label = { t('dialog.Cancel') }
<button
className = 'as-link'
onClick = { onTogglePasswordEditState }
onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
role = 'button'
tabIndex = { 0 }>{ t('dialog.Cancel') }</a>
<a
aria-label = { t('dialog.add') }
type = 'button'>
{ t('dialog.Cancel') }
<span className = 'sr-only'>({ t('dialog.password') })</span>
</button>
<button
className = 'as-link'
onClick = { onPasswordSave }
onKeyPress = { onPasswordSaveKeyPressHandler }
role = 'button'
tabIndex = { 0 }>{ t('dialog.add') }</a>
type = 'button'>
{ t('dialog.add') }
<span className = 'sr-only'>({ t('dialog.password') })</span>
</button>
</>
);
}
@@ -308,49 +221,44 @@ function PasswordSection({
if (locked) {
return (
<>
<a
aria-label = { t('dialog.Remove') }
className = 'remove-password'
<button
className = 'remove-password as-link'
onClick = { onPasswordRemove }
onKeyPress = { onPasswordRemoveKeyPressHandler }
role = 'button'
tabIndex = { 0 }>{ t('dialog.Remove') }</a>
type = 'button'>
{ t('dialog.Remove') }
<span className = 'sr-only'>({ t('dialog.password') })</span>
</button>
{
// There are cases like lobby and grant moderator when password is not available
password ? <>
<a
aria-label = { t('dialog.copy') }
className = 'copy-password'
<button
className = 'copy-password as-link'
onClick = { onPasswordCopy }
onKeyPress = { onPasswordCopyKeyPressHandler }
role = 'button'
tabIndex = { 0 }>{ t('dialog.copy') }</a>
type = 'button'>
{ t('dialog.copy') }
<span className = 'sr-only'>({ t('dialog.password') })</span>
</button>
</> : null
}
{locked === LOCKED_LOCALLY && (
<a
aria-label = { t(passwordVisible ? 'dialog.hide' : 'dialog.show') }
<button
className = 'as-link'
onClick = { passwordVisible ? onPasswordHide : onPasswordShow }
onKeyPress = { passwordVisible
? onPasswordHideKeyPressHandler
: onPasswordShowKeyPressHandler
}
role = 'button'
tabIndex = { 0 }>{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}</a>
type = 'button'>
{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}
<span className = 'sr-only'>({ t('dialog.password') })</span>
</button>
)}
</>
);
}
return (
<a
aria-label = { t('info.addPassword') }
className = 'add-password'
<button
className = 'add-password as-link'
onClick = { onTogglePasswordEditState }
onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
role = 'button'
tabIndex = { 0 }>{ t('info.addPassword') }</a>
type = 'button'>{ t('info.addPassword') }</button>
);
}

View File

@@ -48,7 +48,7 @@ export function openLogoutDialog(onLogout: Function) {
* welcome page or not.
* @returns {Function}
*/
export function openSettingsDialog(defaultTab: string, isDisplayedOnWelcomePage?: boolean) {
export function openSettingsDialog(defaultTab?: string, isDisplayedOnWelcomePage?: boolean) {
return openDialog(SettingsDialog, {
defaultTab,
isDisplayedOnWelcomePage

View File

@@ -254,6 +254,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
return (
<Select
id = 'more-maxStageParticipants-select'
label = { t('settings.maxStageParticipants') }
onChange = { this._onMaxStageParticipantsSelect }
options = { maxParticipantsItems }
@@ -286,6 +287,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
return (
<Select
className = { classes.bottomMargin }
id = 'more-language-select'
label = { t('settings.language') }
onChange = { this._onLanguageItemSelect }
options = { languageItems }

View File

@@ -6,7 +6,6 @@ import { translate } from '../../../base/i18n/functions';
import { IconGear } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { openSettingsDialog } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
/**
* The type of the React {@code Component} props of {@link SettingsButton}.
@@ -41,10 +40,10 @@ class SettingsButton extends AbstractButton<IProps> {
* @returns {void}
*/
_handleClick() {
const { defaultTab = SETTINGS_TABS.AUDIO, dispatch, isDisplayedOnWelcomePage = false } = this.props;
const { dispatch, isDisplayedOnWelcomePage = false } = this.props;
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));
dispatch(openSettingsDialog(undefined, isDisplayedOnWelcomePage));
}
}

View File

@@ -192,7 +192,6 @@ const AudioSettingsContent = ({
jitsiTrack = { jitsiTrack }
key = { `me-${index}` }
length = { length }
listHeaderId = { microphoneHeaderId }
measureAudioLevels = { measureAudioLevels }
onClick = { _onMicrophoneEntryClick }>
{label}
@@ -221,7 +220,6 @@ const AudioSettingsContent = ({
isSelected = { isSelected }
key = { key }
length = { length }
listHeaderId = { speakerHeaderId }
onClick = { _onSpeakerEntryClick }>
{label}
</SpeakerEntry>

View File

@@ -54,8 +54,6 @@ interface IProps {
length: number;
listHeaderId: string;
/**
* Used to decide whether to listen to audio level changes.
*/
@@ -112,7 +110,6 @@ const MicrophoneEntry = ({
isSelected,
length,
jitsiTrack,
listHeaderId,
measureAudioLevels,
onClick: propsClick
}: IProps) => {
@@ -138,7 +135,7 @@ const MicrophoneEntry = ({
* @returns {void}
*/
const onKeyPress = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ') {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
propsClick(deviceId);
}
@@ -190,14 +187,9 @@ const MicrophoneEntry = ({
activeTrackRef.current = jitsiTrack;
}, [ jitsiTrack ]);
const deviceTextId = `choose_microphone${deviceId}`;
const labelledby = `${listHeaderId} ${deviceTextId} `;
return (
<li
aria-checked = { isSelected }
aria-labelledby = { labelledby }
aria-posinset = { index }
aria-setsize = { length }
className = { classes.container }
@@ -206,7 +198,7 @@ const MicrophoneEntry = ({
role = 'radio'
tabIndex = { 0 }>
<ContextMenuItem
accessibilityLabel = ''
accessibilityLabel = { children }
icon = { isSelected ? IconCheck : undefined }
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
selected = { isSelected }

View File

@@ -39,8 +39,6 @@ interface IProps {
*/
length: number;
listHeaderId: string;
/**
* Click handler for the component.
*/
@@ -111,7 +109,7 @@ const SpeakerEntry = (props: IProps) => {
* @returns {void}
*/
function _onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ') {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
props.onClick(props.deviceId);
}
@@ -135,15 +133,12 @@ const SpeakerEntry = (props: IProps) => {
}
}
const { children, isSelected, index, deviceId, length, listHeaderId } = props;
const deviceTextId = `choose_speaker${deviceId}`;
const labelledby = `${listHeaderId} ${deviceTextId} `;
const { children, isSelected, index, length } = props;
/* eslint-disable react/jsx-no-bind */
return (
<li
aria-checked = { isSelected }
aria-labelledby = { labelledby }
aria-posinset = { index }
aria-setsize = { length }
className = { classes.container }
@@ -152,7 +147,7 @@ const SpeakerEntry = (props: IProps) => {
role = 'radio'
tabIndex = { 0 }>
<ContextMenuItem
accessibilityLabel = ''
accessibilityLabel = { children }
icon = { isSelected ? IconCheck : undefined }
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
selected = { isSelected }

View File

@@ -302,7 +302,7 @@ const VideoSettingsContent = ({
</ContextMenuItemGroup>
<ContextMenuItemGroup>
{ virtualBackgroundSupported && <ContextMenuItem
accessibilityLabel = 'virtualBackground.title'
accessibilityLabel = { t('virtualBackground.title') }
icon = { IconImage }
onClick = { selectBackground }
text = { t('virtualBackground.title') } /> }

View File

@@ -84,15 +84,16 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
titleKey = 'dialog.shareVideoTitle'>
<Input
autoFocus = { true }
bottomLabel = { error && t('dialog.sharedVideoDialogError') }
className = 'dialog-bottom-margin'
error = { error }
id = 'shared-video-url-input'
label = { t('dialog.videoLink') }
name = 'sharedVideoUrl'
onChange = { this._onChange }
placeholder = { t('dialog.sharedVideoLinkPlaceholder') }
type = 'text'
value = { this.state.value } />
{ error && <span className = 'shared-video-dialog-error'>{ t('dialog.sharedVideoDialogError') }</span> }
</Dialog>
);
}

View File

@@ -45,7 +45,7 @@ const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) =>
}, [ language ]);
const onSourceLanguageClick = useCallback(() => {
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
dispatch(openSettingsDialog(SETTINGS_TABS.MORE, false));
}, []);
return (

View File

@@ -22,6 +22,11 @@ interface IProps {
*/
getRef?: Function;
/**
* Function called when the portal target becomes actually visible.
*/
onVisible?: Function;
/**
* Function used to get the updated size info of the container on it's resize.
*/
@@ -45,7 +50,7 @@ interface IProps {
*
* @returns {ReactElement}
*/
function DialogPortal({ children, className, style, getRef, setSize, targetSelector }: IProps) {
function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) {
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
@@ -89,6 +94,7 @@ function DialogPortal({ children, className, style, getRef, setSize, targetSelec
clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
portalTarget.style.visibility = 'visible';
onVisible?.();
}, 100);
}
});

View File

@@ -1,5 +1,5 @@
import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
import ReactFocusLock from 'react-focus-lock';
import { FocusOn } from 'react-focus-on';
import { makeStyles } from 'tss-react/mui';
import { isElementInTheViewport } from '../../../base/ui/functions.web';
@@ -102,12 +102,7 @@ function Drawer({
<div
className = { `drawer-menu ${styles.drawer} ${className}` }
onClick = { handleInsideClick }>
<ReactFocusLock
lockProps = {{
role: 'dialog',
'aria-modal': true,
'aria-labelledby': `#${headingId}`
}}
<FocusOn
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
@@ -118,8 +113,15 @@ function Drawer({
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
{children}
</ReactFocusLock>
<div
aria-labelledby = { headingId ? `#${headingId}` : undefined }
aria-modal = { true }
data-autofocus = { true }
role = 'dialog'
tabIndex = { -1 }>
{children}
</div>
</FocusOn>
</div>
</div>
) : null

View File

@@ -8,7 +8,7 @@ import {
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import './subscriber';
import './subscriber.web';
/**
* Middleware which intercepts Toolbox actions to handle changes to the
@@ -18,7 +18,6 @@ import './subscriber';
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CLEAR_TOOLBOX_TIMEOUT: {
const { timeoutID } = store.getState()['features/toolbox'];
@@ -45,7 +44,6 @@ MiddlewareRegistry.register(store => next => action => {
});
type DocumentElement = {
mozRequestFullScreen?: Function;
requestFullscreen?: Function;
webkitRequestFullscreen?: Function;
};
@@ -63,34 +61,26 @@ type DocumentElement = {
function _setFullScreen(next: Function, action: AnyAction) {
const result = next(action);
if (typeof APP === 'object') {
const { fullScreen } = action;
const { fullScreen } = action;
if (fullScreen) {
const documentElement: DocumentElement
= document.documentElement || {};
if (fullScreen) {
const documentElement: DocumentElement
= document.documentElement || {};
if (typeof documentElement.requestFullscreen === 'function') {
documentElement.requestFullscreen();
} else if (
typeof documentElement.mozRequestFullScreen === 'function') {
documentElement.mozRequestFullScreen();
} else if (
typeof documentElement.webkitRequestFullscreen === 'function') {
documentElement.webkitRequestFullscreen();
}
return result;
if (typeof documentElement.requestFullscreen === 'function') {
documentElement.requestFullscreen();
} else if (
typeof documentElement.webkitRequestFullscreen === 'function') {
documentElement.webkitRequestFullscreen();
}
if (typeof document.exitFullscreen === 'function') {
document.exitFullscreen();
return result;
}
} else if (typeof document.mozCancelFullScreen === 'function') {
document.mozCancelFullScreen();
} else if (typeof document.webkitExitFullscreen === 'function') {
document.webkitExitFullscreen();
}
if (typeof document.exitFullscreen === 'function') {
document.exitFullscreen();
} else if (typeof document.webkitExitFullscreen === 'function') {
document.webkitExitFullscreen();
}
return result;

View File

@@ -15,10 +15,6 @@ import { isAudioMuteButtonDisabled } from './functions.any';
StateListenerRegistry.register(
/* selector */ (state: IReduxState) => isAudioMuteButtonDisabled(state),
/* listener */ (disabled: boolean, store: IStore, previousDisabled: boolean) => {
if (typeof APP !== 'object') {
return;
}
if (disabled !== previousDisabled) {
APP.API.notifyAudioAvailabilityChanged(!disabled);
}

View File

@@ -143,7 +143,9 @@ function Slider({ ariaLabel, max, min, onChange, step, value }: IProps) {
return (
<div className = { classes.sliderContainer }>
<ul className = { cx('empty-list', classes.knobContainer) }>
<ul
aria-hidden = { true }
className = { cx('empty-list', classes.knobContainer) }>
{knobs.map((_, i) => (
<li
className = { classes.knob }

View File

@@ -81,6 +81,7 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<IProps> {
content = { t(tooltipKey) }
position = { 'bottom' }>
<Label
accessibilityText = { t(tooltipKey) }
className = { className }
color = { COLORS.white }
icon = { icon }

View File

@@ -187,9 +187,15 @@ class VideoQualitySlider extends Component<IProps> {
return (
<div className = { clsx('video-quality-dialog', classes.dialog) }>
<div className = { classes.dialogDetails }>{t('videoStatus.adjustFor')}</div>
<div
aria-hidden = { true }
className = { classes.dialogDetails }>
{t('videoStatus.adjustFor')}
</div>
<div className = { classes.dialogContents }>
<div className = { classes.sliderDescription }>
<div
aria-hidden = { true }
className = { classes.sliderDescription }>
<span>{t('videoStatus.bestPerformance')}</span>
<span>{t('videoStatus.highestQuality')}</span>
</div>

View File

@@ -122,7 +122,6 @@ function UploadImageButton({
return (
<>
{showLabel && <label
aria-label = { t('virtualBackground.uploadImage') }
className = { classes.label }
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }

View File

@@ -360,6 +360,24 @@ function VirtualBackgrounds({
await setPreviewIsLoaded(loaded);
}, []);
// create a full list of {backgroundId: backgroundLabel} to easily retrieve label of selected background
const labelsMap: Record<string, string> = {
none: t('virtualBackground.none'),
'slight-blur': t('virtualBackground.slightBlur'),
blur: t('virtualBackground.blur'),
..._images.reduce<Record<string, string>>((acc, image) => {
acc[image.id] = image.tooltip ? t(`virtualBackground.${image.tooltip}`) : '';
return acc;
}, {}),
...storedImages.reduce<Record<string, string>>((acc, image, index) => {
acc[image.id] = t('virtualBackground.uploadedImage', { index: index + 1 });
return acc;
}, {})
};
const currentBackgroundLabel = labelsMap[selectedThumbnail] || labelsMap.none;
return (
<>
<VirtualBackgroundPreview
@@ -372,6 +390,13 @@ function VirtualBackgrounds({
</div>
) : (
<div className = { classes.container }>
<span
className = 'sr-only'
id = 'virtual-background-current-info'>
{ t('virtualBackground.accessibilityLabel.currentBackground', {
background: currentBackgroundLabel
}) }
</span>
{_showUploadButton
&& <UploadImageButton
setLoading = { setLoading }
@@ -380,6 +405,8 @@ function VirtualBackgrounds({
showLabel = { previewIsLoaded }
storedImages = { storedImages } />}
<div
aria-describedby = 'virtual-background-current-info'
aria-label = { t('virtualBackground.accessibilityLabel.selectBackground') }
className = { classes.thumbnailContainer }
role = 'radiogroup'
tabIndex = { -1 }>

View File

@@ -5,6 +5,8 @@ local log = module._log;
local host = module.host;
local st = require "util.stanza";
local um_is_admin = require "core.usermanager".is_admin;
local jid_split = require 'util.jid'.split;
local jid_bare = require 'util.jid'.bare;
local function is_admin(jid)
@@ -39,8 +41,11 @@ module:log("debug",
-- option to disable room modification (sending muc config form) for guest that do not provide token
local require_token_for_moderation;
-- option to allow domains to skip token verification
local allowlist;
local function load_config()
require_token_for_moderation = module:get_option_boolean("token_verification_require_token_for_moderation");
allowlist = module:get_option_set('token_verification_allowlist', {});
end
load_config();
@@ -57,6 +62,17 @@ local function verify_user(session, stanza)
return true;
end
-- token not required for users matching allow list
local user_bare_jid = jid_bare(user_jid);
local _, user_domain = jid_split(user_jid);
-- allowlist for participants
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) then
module:log("debug", "Token not required from user in allow list: %s", user_jid);
return true;
end
module:log("debug",
"Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to);
if not token_util:verify_room(session, stanza.attr.to) then