mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-24 16:57:46 +00:00
Compare commits
21 Commits
7330
...
token-veri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de81d395c1 | ||
|
|
9af56d52c2 | ||
|
|
2a9c40f0d2 | ||
|
|
3ae18be21f | ||
|
|
9f5dbb21a7 | ||
|
|
2d14990b9e | ||
|
|
169c8ecb62 | ||
|
|
d608cf40f5 | ||
|
|
51a4e7daa3 | ||
|
|
7538bfc713 | ||
|
|
48e1f443ea | ||
|
|
2292ebe762 | ||
|
|
5425b52615 | ||
|
|
74f605e045 | ||
|
|
1918566581 | ||
|
|
ee8ba6696d | ||
|
|
15df3cb11e | ||
|
|
b77db024f5 | ||
|
|
c8a87e368a | ||
|
|
277ca23c52 | ||
|
|
55f66e236e |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
&-actions {
|
||||
margin-top: 10px;
|
||||
a {
|
||||
button {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
1
modules/API/external/external_api.js
vendored
1
modules/API/external/external_api.js
vendored
@@ -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',
|
||||
|
||||
@@ -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
184
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,6 +103,7 @@ const BaseIndicator = ({
|
||||
className = { className }
|
||||
id = { id }>
|
||||
<Icon
|
||||
alt = { t(tooltipKey) }
|
||||
className = { iconClassName }
|
||||
color = { iconColor }
|
||||
id = { iconId }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -32,6 +32,7 @@ const RaisedHandsCountLabel = () => {
|
||||
content = { t('raisedHandsLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t('raisedHandsLabel') }
|
||||
className = { styles.label }
|
||||
icon = { IconRaiseHand }
|
||||
iconColor = { theme.palette.icon04 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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() } />
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -127,6 +127,7 @@ function MeetingParticipants({
|
||||
<Input
|
||||
className = { styles.search }
|
||||
clearable = { true }
|
||||
id = 'participants-search-input'
|
||||
onChange = { setSearchString }
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -70,6 +70,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
||||
},
|
||||
|
||||
get mediaType() {
|
||||
if (this.selfRecording.on && !this.selfRecording.withVideo) {
|
||||
return 'audio/webm;';
|
||||
}
|
||||
if (!preferredMediaType) {
|
||||
preferredMediaType = getMimeType();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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') } /> }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -122,7 +122,6 @@ function UploadImageButton({
|
||||
return (
|
||||
<>
|
||||
{showLabel && <label
|
||||
aria-label = { t('virtualBackground.uploadImage') }
|
||||
className = { classes.label }
|
||||
htmlFor = 'file-upload'
|
||||
onKeyPress = { uploadImageKeyPress }
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user