Compare commits

...

21 Commits
8317 ... 8335

Author SHA1 Message Date
Hristo Terezov
fb8e451e2e feat(customParticipantButton): metrics 2025-01-08 11:11:21 -06:00
damencho
5dd7944bdb fix(connection): Adds default tenant value on error.
Avoids undefined error.
2025-01-08 08:36:35 -06:00
Jaya Allamsetty
48732c6784 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1890.0.0+144b0cab...v1893.0.0+496b64a7
2025-01-07 21:45:03 -06:00
Hristo Terezov
97146ed8a7 fix(configWhitelist): Remove some props. 2025-01-07 15:32:08 -06:00
Дамян Минков
78b17c8d17 feat(tests): Adds self view, display name and end conference tests. (#15432)
* feat(tests): Use shorter display names for screenshots.

* feat(tests): Adds self view tests.

* feat(tests): Adds display name test.

* feat(tests): Adds end conference test.
2025-01-07 14:55:43 -06:00
Saúl Ibarra Corretgé
9732d1be86 fix(polls) improve resilience to bogus data 2025-01-07 16:06:10 +01:00
Hristo Terezov
a836187620 feat(URL-overrides): Add metrics. 2025-01-06 09:35:36 -06:00
Wilson Furtado
a0cad4c595 fix(notification) add tabIndex to close Icon in the notification Item 2025-01-06 10:13:06 +01:00
Wilson Furtado
19b9eac84a fix(input) fix content overlapping when clearable icon is present 2025-01-06 10:12:14 +01:00
Wilson Furtado
068e33fc0f fix(toolbar): Removed unnecessary re render of the reactions component (#15433)
Added usememo to remove unnecessary re render. Fixes #15434.
2024-12-31 10:04:47 -06:00
Wilson Furtado
00c6bee2fd feat(welcome-page): added hover fill to trash icon (#15431)
Fixes #15430.
2024-12-30 16:11:43 -06:00
Wilson Furtado
748ead7e13 feat: Added hover styles and added tabIndex to the meeting list container instead of the meeting item header (#15429)
* added hover styles and added tabIndex to the container instead of the meeting heading

* lint error fix
2024-12-30 14:05:56 -06:00
damencho
174c4418fb fix(prosody): Adds another condition to the filter. 2024-12-26 12:12:53 -06:00
Damien Fetis
64494cab81 lang: Update missing french labels (#15422) 2024-12-23 18:28:35 -06:00
damencho
50d0092e30 fix(shared-video): Remove disable button action from web. 2024-12-23 07:46:25 -06:00
damencho
fdbd7239ff fix(shared-video): Gets from info from the incoming presence.
Ignore using from field send in attributes of the command.
2024-12-23 07:46:25 -06:00
Дамян Минков
a30958ab23 feat(tests): Adds chat panel and codec selection tests. (#15416)
* fix(tests): Another attempt to fix Firefox excludes.

Drawback is that it will be a little bit slow.

* feat(tests): Adds chatPanel tests.

* feat(tests): Adds codec selection tests.
2024-12-20 17:04:07 -06:00
damencho
aca55172e4 fix(config): Fixes wrong file location. 2024-12-20 11:37:48 -06:00
Дамян Минков
c6cce9253c feat(tests): Adds breakout tests. (#15414)
* feat(tests): Introduces BasePageObject.

* fix(tests): Use wdio aria selector where possible.

* fix(tests): Correct test exclusion for Firefox.

* fix(tests): Rearrange code.

* feat(tests): Adds breakout tests.
2024-12-20 06:17:49 -06:00
Saúl Ibarra Corretgé
c23684e11c fix(devices) filter out Zoom audio device
I haven't seen any particular problem with it, but it's of no use
anyway...
2024-12-20 07:58:48 +01:00
luzpaz
30595584a3 chore(typos) fix various typos
Found via `codespell -q 3 -S "*.svg,./lang" -L anser,bu,dialin,goup,miliseconds,nd,vew`
2024-12-20 07:58:31 +01:00
91 changed files with 1745 additions and 382 deletions

View File

@@ -42,7 +42,7 @@ public class JitsiMeetActivityDelegate {
/**
* Tells whether or not the permissions request is currently in progress.
*
* @return {@code true} if the permssions are being requested or {@code false} otherwise.
* @return {@code true} if the permissions are being requested or {@code false} otherwise.
*/
static boolean arePermissionsBeingRequested() {
return permissionListener != null;

View File

@@ -459,7 +459,7 @@ var config = {
// // Translation languages.
// // Available languages can be found in
// // ./src/react/features/transcribing/translation-languages.json.
// // ./lang/translation-languages.json.
// translationLanguages: ['en', 'es', 'fr', 'ro'],
// // Important languages to show on the top of the language list.
@@ -907,7 +907,7 @@ var config = {
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
// buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the
// corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a cerain
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a certain
// configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// mainToolbarButtons: [
@@ -1735,7 +1735,7 @@ var config = {
// 'notify.participantsWantToJoin', // shown when lobby is enabled and participants request to join meeting
// 'notify.passwordRemovedRemotely', // shown when a password has been removed remotely
// 'notify.passwordSetRemotely', // shown when a password has been set remotely
// 'notify.raisedHand', // shown when a partcipant used raise hand,
// 'notify.raisedHand', // shown when a participant used raise hand,
// 'notify.screenShareNoAudio', // shown when the audio could not be shared for the selected screen
// 'notify.screenSharingAudioOnlyTitle', // shown when the best performance has been affected by screen sharing
// 'notify.selfViewTitle', // show "You can always un-hide the self-view from settings"
@@ -1766,7 +1766,7 @@ var config = {
// disableFilmstripAutohiding: false,
// filmstrip: {
// // Disable the vertical/horizonal filmstrip.
// // Disable the vertical/horizontal filmstrip.
// disabled: false,
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.

View File

@@ -19,11 +19,11 @@
flex-direction: column;
.description {
color: #2f3237;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
max-width: 436px;
color: #2f3237;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
max-width: 436px;
}
}
@@ -127,7 +127,8 @@
cursor: pointer;
}
&.with-click-handler:hover {
&.with-click-handler:hover,
&.with-click-handler:focus {
background-color: #c7ddff;
}
@@ -155,14 +156,22 @@
margin-right: 16px;
position: absolute;
&> svg {
&>svg {
fill: #0074e0;
}
}
.item:hover, .item:focus, .item:focus-within {
.item:hover,
.item:focus,
.item:focus-within {
.delete-meeting {
display: block;
}
.delete-meeting:hover {
&>svg {
fill: #4687ED;
}
}
}
}
}

View File

@@ -69,7 +69,7 @@ platform :ios do
end
)
# Inrement the build number by 1
# Increment the build number by 1
increment_build_number(
build_number: Time.now.to_i,
xcodeproj: "app/app.xcodeproj"

View File

@@ -334,6 +334,7 @@
"kickParticipantButton": "Expulser",
"kickParticipantDialog": "Êtes-vous sûr(e) de vouloir expulser ce participant ?",
"kickParticipantTitle": "Expulser ce participant ?",
"kickSystemTitle": "Oups ! Vous avez été expulsé de la réunion",
"kickTitle": "Oups ! vous avez été expulsé(e) par {{participantDisplayName}}",
"linkMeeting": "Relier la conférence",
"linkMeetingTitle": "Relier la conférence à Salesforce",
@@ -439,7 +440,10 @@
"shareScreenWarningD2": "vous devez arrêter le partage d'audio, démarrer le partage d'écran et cocher l'option \"Partager l'audio\".",
"shareScreenWarningH1": "Si vous voulez partager uniquement votre écran:",
"shareScreenWarningTitle": "Vous devez cesser de partager votre audio avant de partager votre écran",
"shareVideoConfirmPlay": "Vous êtes sur le point d'ouvrir un site web externe. Voulez-vous continuer ?",
"shareVideoConfirmPlayTitle": "{{name}} a partagé une vidéo avec vous.",
"shareVideoLinkError": "Veuillez renseigner un lien de diffusion vidéo fonctionnel.",
"shareVideoLinkStopped": "La vidéo de {{name}} a été arrêtée.",
"shareVideoTitle": "Partager une vidéo",
"shareYourScreen": "Partager votre écran",
"shareYourScreenDisabled": "Le partage d'écran est désactivé.",
@@ -638,6 +642,7 @@
"on": "En direct",
"onBy": "{{name}} a démarré la diffusion en direct",
"pending": "Lancement du direct…",
"policyError": "Vous avez essayé de démarrer une diffusion en direct trop rapidement. Veuillez réessayer plus tard !",
"serviceName": "Service de diffusion en direct",
"sessionAlreadyActive": "Cette session est déjà en cours d'enregistrement ou de diffusion.",
"signIn": "Se connecter avec Google",
@@ -736,6 +741,7 @@
"connectedOneMember": "{{name}} a rejoint la réunion",
"connectedThreePlusMembers": "{{name}} et {{count}} autres personnes ont rejoint la réunion",
"connectedTwoMembers": "{{first}} et {{second}} ont rejoint la réunion",
"connectionFailed": "Connexion échouée. Veuillez réessayer plus tard !",
"dataChannelClosed": "Qualité vidéo dégradée",
"dataChannelClosedDescription": "Le canal de communication avec le Bridge a été interrompu, la qualité vidéo se trouve limitée à sa valeur la plus faible.",
"dataChannelClosedDescriptionWithAudio": "Le canal de pont est fermé, ce qui peut entraîner des perturbations de l'audio et de la vidéo.",
@@ -750,6 +756,9 @@
"gifsMenu": "GIPHY",
"groupTitle": "Notifications",
"hostAskedUnmute": "Le modérateur souhaite vous donner la parole",
"invalidTenant": "Tenant invalide",
"invalidTenantHyphenDescription": "Le tenant que vous utilisez est invalide (commence ou se termine par '-').",
"invalidTenantLengthDescription": "Le tenant que vous utilisez est trop long.",
"invitedOneMember": "{{name}} a été invité(e)",
"invitedThreePlusMembers": "{{name}} et {{count}} autres ont été invités",
"invitedTwoMembers": "{{first}} et {{second}} ont été invités",
@@ -786,6 +795,7 @@
"newDeviceAction": "Utiliser",
"newDeviceAudioTitle": "Nouveau périphérique audio détecté",
"newDeviceCameraTitle": "Nouvelle caméra détectée",
"nextToSpeak": "Vous êtes le prochain à prendre la parole",
"noiseSuppressionDesktopAudioDescription": "La suppression de bruit ne peut pas être activée en même temps que la partage audio du système, veuillez le désactiver et réessayer.",
"noiseSuppressionFailedTitle": "Échec du démarrage de la suppression de bruit",
"noiseSuppressionStereoDescription": "La suppression de bruit dune source stéréo nest pas encore supportée.",
@@ -820,6 +830,7 @@
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système.",
"videoUnmuteBlockedTitle": "Rétablissement de la caméra bloqué !",
"viewLobby": "Voir la salle d'attente",
"viewParticipants": "Voir les participants",
"viewVisitors": "Voir les visiteurs",
"waitingParticipants": "{{waitingParticipants}} personnes",
"waitingVisitors": "Visiteurs en attente dans la file : {{waitingVisitors}}",
@@ -839,6 +850,8 @@
"breakoutRooms": "Salles annexes",
"goLive": "Passer en direct",
"invite": "Inviter quelqu'un",
"lowerAllHands": "Abaisser toutes les mains",
"lowerHand": "Abaisser la main",
"moreModerationActions": "Options de modération supplémentaires",
"moreModerationControls": "Options de modération supplémentaires",
"moreParticipantOptions": "Options supplémentaires pour les participants",
@@ -875,6 +888,7 @@
"submit": "Envoyer"
},
"by": "Par {{ name }}",
"closeButton": "Fermer le sondage",
"create": {
"addOption": "Ajouter une option",
"answerPlaceholder": "Option {{index}}",
@@ -914,9 +928,11 @@
"configuringDevices": "Configuration des appareils…",
"connectedWithAudioQ": "Êtes-vous connecté avec le microphone ?",
"connection": {
"failed": "Le test de connexion a échoué !",
"good": "Votre connexion Internet est bonne !",
"nonOptimal": "Votre connexion n'est pas optimale",
"poor": "Vous avez une mauvaise connexion"
"poor": "Vous avez une mauvaise connexion",
"running": "Exécution du test de connexion…"
},
"connectionDetails": {
"audioClipping": "Attendez vous à ce que votre son soit coupé.",
@@ -925,6 +941,7 @@
"goodQuality": "Impressionnant ! La qualité de vos médias sera excellente",
"noMediaConnectivity": "Nous n'avons pas pu trouver un moyen d'établir une connectivité multimédia pour ce test. Cela est généralement causé par un pare-feu ou un NAT.",
"noVideo": "Attendez vous à ce que votre qualité vidéo soit très mauvaise.",
"testFailed": "Le test de connexion a rencontré des problèmes inattendus, mais cela pourrait ne pas affecter votre expérience.",
"undetectable": "Si vous ne parvenez toujours pas à passer des appels dans le navigateur, nous vous recommandons de vous assurer que vos haut-parleurs, microphone et caméra sont correctement configurés, que vous avez accordé à votre navigateur les droits d'utiliser votre microphone et votre caméra et que la version de votre navigateur est à jour. Si vous rencontrez toujours des difficultés pour appeler, vous devez contacter le développeur de l'application Web.",
"veryPoorConnection": "Attendez vous à ce que la qualité de votre appel soit très mauvaise",
"videoFreezing": "Attendez vous à ce que votre vidéo saute, soit noire, et pixelisée.",
@@ -1040,6 +1057,7 @@
"onBy": "{{name}} a démarré l'enregistrement",
"onlyRecordSelf": "Enregistrer seulement mon audio et ma vidéo.",
"pending": "Préparation de l'enregistrement de la réunion…",
"policyError": "Vous avez essayé de démarrer un enregistrement trop rapidement. Veuillez réessayer plus tard !",
"recordAudioAndVideo": "Enregistrer l'audio et la vidéo",
"recordTranscription": "Enregistrer la transcription",
"saveLocalRecording": "Sauvegarder lenregistrement local (Beta)",
@@ -1088,6 +1106,7 @@
"desktopShareWarning": "Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
"devices": "Périphériques",
"followMe": "Tout le monde me suit",
"followMeRecorder": "L'enregistreur me suit",
"framesPerSecond": "images par seconde",
"incomingMessage": "un message arrive",
"language": "Langue",
@@ -1234,6 +1253,7 @@
"lobbyButton": "Activer / Désactiver le mode salle d'attente",
"localRecording": "Activer / Désactiver les contrôles d'enregistrement local",
"lockRoom": "Activer / Désactiver le mot de passe de la réunion",
"love": "Cœur",
"lowerHand": "Baisser la main",
"moreActions": "Activer / Désactiver le menu d'actions supplémentaires",
"moreActionsMenu": "Menu d'actions supplémentaires",
@@ -1251,6 +1271,7 @@
"privateMessage": "Envoyer un message privé",
"profile": "Éditer votre profil",
"raiseHand": "Lever la main",
"react": "Réactions aux messages",
"reactions": "Réactions",
"reactionsMenu": "Ouvrir / fermer le menu réactions",
"recording": "Activer / Désactiver l'enregistrement",
@@ -1322,6 +1343,7 @@
"lobbyButtonEnable": "Activer le mode salle d'attente / contrôle des participant(e)s",
"login": "Connexion",
"logout": "Déconnexion",
"love": "Cœur",
"lowerYourHand": "Baisser la main",
"moreActions": "Plus d'actions",
"moreOptions": "Plus d'options",
@@ -1347,6 +1369,7 @@
"raiseYourHand": "Lever la main",
"reactionBoo": "Envoyer réaction huer",
"reactionClap": "Envoyer réaction applaudir",
"reactionHeart": "Envoyer une réaction en forme de cœur",
"reactionLaugh": "Envoyer réaction rire",
"reactionLike": "Envoyer réaction approuver",
"reactionSilence": "Envoyer réaction silence",
@@ -1380,7 +1403,7 @@
"transcribing": {
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
"expandedLabel": "La transcription est actuellement activée",
"failedToStart": "Échec de démarrage de la transcription",
"failed": "La transcription a échoué",
"labelToolTip": "La transcription de la réunion est en cours",
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
"sourceLanguageHere": "ici",
@@ -1481,10 +1504,15 @@
},
"visitors": {
"chatIndicator": "(visiteur)",
"labelTooltip": "Nombre de Visiteurs",
"joinMeeting": {
"description": "Vous êtes actuellement un observateur dans cette conférence.",
"raiseHand": "Levez la main",
"title": "Rejoindre la réunion",
"wishToSpeak": "Si vous souhaitez prendre la parole, veuillez lever la main ci-dessous et attendre l'approbation du modérateur."
},
"labelTooltip": "Nombre de Visiteurs: {{count}}",
"notification": {
"demoteDescription": "Envoyé ici par {{actor}}, levez la main pour participer",
"description": "Pour participer lever la main.",
"noMainParticipantsDescription": "Un participant doit démarrer la réunion. Veuillez réessayer dans un moment.",
"noMainParticipantsTitle": "Cette réunion n'a pas encore commencé.",
"noVisitorLobby": "Vous ne pouvez pas rejoindre tant qu'une salle d'attente est activée pour la réunion.",

View File

@@ -115,7 +115,7 @@ import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/functions';
@@ -1571,8 +1571,8 @@ class API {
formattedArgument += `${arg.toString()}: ${arg.stack}`;
} else if (typeof arg === 'object') {
// NOTE: The non-enumerable properties of the objects wouldn't be included in the string after
// JSON.strigify. For example Map instance will be translated to '{}'. So I think we have to eventually
// do something better for parsing the arguments. But since this option for strigify is part of the
// JSON.stringify. For example Map instance will be translated to '{}'. So I think we have to eventually
// do something better for parsing the arguments. But since this option for stringify is part of the
// public interface and I think it could be useful in some cases I will it for now.
try {
formattedArgument += JSON.stringify(arg);

24
package-lock.json generated
View File

@@ -62,7 +62,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1893.0.0+496b64a7/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -136,6 +136,7 @@
"@types/js-md5": "0.4.3",
"@types/jsonwebtoken": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/minimatch": "5.1.2",
"@types/mocha": "10.0.10",
"@types/moment-duration-format": "2.2.6",
"@types/offscreencanvas": "2019.7.2",
@@ -7176,6 +7177,13 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mocha": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -16417,8 +16425,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"integrity": "sha512-/NoS/uUJvMXdbi5gQoihcQ9ovXlQtFKgdmtiLqLJxz91T9JwdVZNsjo3G6RUVIJ07myb/vjQp91kpm45U7CgIw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1893.0.0+496b64a7/lib-jitsi-meet.tgz",
"integrity": "sha512-dNsspL+Kmji501AOY8CbatCifk/CKPKfqurld4o22a9SmQJKM7PfYtCNu+o2wUae1UnWHuf65gpnHNxGpC/uOA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -29785,6 +29793,12 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"@types/mocha": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -36497,8 +36511,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"integrity": "sha512-/NoS/uUJvMXdbi5gQoihcQ9ovXlQtFKgdmtiLqLJxz91T9JwdVZNsjo3G6RUVIJ07myb/vjQp91kpm45U7CgIw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1893.0.0+496b64a7/lib-jitsi-meet.tgz",
"integrity": "sha512-dNsspL+Kmji501AOY8CbatCifk/CKPKfqurld4o22a9SmQJKM7PfYtCNu+o2wUae1UnWHuf65gpnHNxGpC/uOA==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1893.0.0+496b64a7/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -142,6 +142,7 @@
"@types/js-md5": "0.4.3",
"@types/jsonwebtoken": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/minimatch": "5.1.2",
"@types/mocha": "10.0.10",
"@types/moment-duration-format": "2.2.6",
"@types/offscreencanvas": "2019.7.2",

View File

@@ -154,6 +154,30 @@ export async function createHandlers({ getState }: IStore) {
return handlers;
}
/**
* Checks whether a url is a data URL or not.
*
* @param {string} url - The URL to be checked.
* @returns {boolean}
*/
function isDataURL(url?: string): boolean {
if (typeof url !== 'string') { // The icon will be ignored
return false;
}
try {
const urlObject = new URL(url);
if (urlObject.protocol === 'data:') {
return false;
}
} catch {
return false;
}
return true;
}
/**
* Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
@@ -185,13 +209,21 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
inIframe?: boolean;
isPromotedFromVisitor?: boolean;
isVisitor?: boolean;
overwritesCustomButtonsWithURL?: boolean;
overwritesCustomParticipantButtonsWithURL?: boolean;
overwritesDefaultLogoUrl?: boolean;
overwritesDeploymentUrls?: boolean;
overwritesEtherpadBase?: boolean;
overwritesHosts?: boolean;
overwritesIceServers?: boolean;
overwritesLiveStreamingUrls?: boolean;
overwritesPeopleSearchUrl?: boolean;
overwritesPrejoinConfigICEUrl?: boolean;
overwritesSalesforceUrl?: boolean;
overwritesSupportUrl?: boolean;
overwritesWatchRTCConfigParams?: boolean;
overwritesWatchRTCProxyUrl?: boolean;
overwritesWatchRTCWSUrl?: boolean;
server?: string;
tenant?: string;
wasLobbyVisible?: boolean;
@@ -235,6 +267,21 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
permanentProperties.overwritesPeopleSearchUrl = 'config.peopleSearchUrl' in params;
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
permanentProperties.overwritesEtherpadBase = 'config.etherpad_base' in params;
const hosts = params['config.hosts'] ?? {};
const hostsProps = [ 'anonymousdomain', 'authdomain', 'domain', 'focus', 'muc', 'visitorFocus' ];
permanentProperties.overwritesHosts = 'config.hosts' in params
|| Boolean(hostsProps.find(p => `config.hosts.${p}` in params || (typeof hosts === 'object' && p in hosts)));
permanentProperties.overwritesWatchRTCConfigParams = 'config.watchRTCConfigParams' in params;
const watchRTCConfigParams = params['config.watchRTCConfigParams'] ?? {};
permanentProperties.overwritesWatchRTCProxyUrl = ('config.watchRTCConfigParams.proxyUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'proxyUrl' in watchRTCConfigParams);
permanentProperties.overwritesWatchRTCWSUrl = ('config.watchRTCConfigParams.wsUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'wsUrl' in watchRTCConfigParams);
const prejoinConfig = params['config.prejoinConfig'] ?? {};
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)
@@ -260,6 +307,18 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
)
);
permanentProperties.overwritesIceServers = Boolean(Object.keys(params).find(k => k.startsWith('iceServers')));
const customToolbarButtons = params['config.customToolbarButtons'] ?? [];
permanentProperties.overwritesCustomButtonsWithURL = Boolean(
customToolbarButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
const customParticipantMenuButtons = params['config.customParticipantMenuButtons'] ?? [];
permanentProperties.overwritesCustomParticipantButtonsWithURL = Boolean(
customParticipantMenuButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
// Optionally, include local deployment information based on the
// contents of window.config.deploymentInfo.
if (deploymentInfo) {

View File

@@ -121,7 +121,7 @@ MiddlewareRegistry.register(store => next => action => {
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// auto redirect is turned on and we have succesfully logged in
// auto redirect is turned on and we have successfully logged in
// let's mark that
dispatch(setTokenAuthUrlSuccess(true));
}

View File

@@ -430,7 +430,7 @@ function _connectionFailed({ dispatch, getState }: IStore, next: Function, actio
} as INotificationProps;
const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { tenant = '' } = parseURIString(locationURL.href) || {};
if (tenant.startsWith('-') || tenant.endsWith('-')) {
notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';

View File

@@ -76,7 +76,6 @@ export default [
'channelLastN',
'connectionIndicators',
'constraints',
'customToolbarButtons',
'brandingRoomAlias',
'debug',
'debugAudioLevels',
@@ -149,7 +148,6 @@ export default [
'enableNoisyMicDetection',
'enableTcc',
'enableAutomaticUrlCopy',
'etherpad_base',
'faceLandmarks',
'failICE',
'feedbackPercentage',
@@ -172,7 +170,6 @@ export default [
'hideAddRoomButton',
'hideEmailInSettings',
'hideLobbyButton',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'ignoreStartMuted',
@@ -236,7 +233,17 @@ export default [
'useTurnUdp',
'videoQuality',
'visitors',
'watchRTCConfigParams',
'watchRTCConfigParams.console',
'watchRTCConfigParams.allowBrowserLogCollection',
'watchRTCConfigParams.collectionInterval',
'watchRTCConfigParams.debug',
'watchRTCConfigParams.keys',
'watchRTCConfigParams.logGetStats',
'watchRTCConfigParams.rtcApiKey',
'watchRTCConfigParams.rtcPeerId',
'watchRTCConfigParams.rtcRoomId',
'watchRTCConfigParams.rtcTags',
'watchRTCConfigParams.rtcToken',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable',
'whiteboard.enabled'

View File

@@ -1,8 +1,11 @@
/**
* Prefixes of devices that will be filtered from the device list.
*
* NOTE: Currently we filter only 'Microsoft Teams Audio Device' virtual device. It seems that it can't be set
* NOTE: It seems that the filtered devices can't be set
* as default device on the OS level and this use case is not handled in the code. If we add more device prefixes that
* can be default devices we should make sure to handle the default device use case.
*/
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [ 'Microsoft Teams Audio Device' ];
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [
'Microsoft Teams Audio Device',
'ZoomAudioDevice'
];

View File

@@ -96,7 +96,7 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
const DEFAULT_INITIAL_PROMISE_STATE = createDeferred<IInitialGUMPromiseResult>();
/**
* Reducer fot the common properties in media state.
* Reducer for the common properties in media state.
*
* @param {ICommonState} state - Common media state.
* @param {Object} action - Action object.

View File

@@ -240,7 +240,7 @@ export function participantJoined(participant: IParticipant) {
// conference. The following check is really necessary because a
// JitsiConference may have moved into leaving but may still manage to
// sneak a PARTICIPANT_JOINED in if its leave is delayed for any purpose
// (which is not outragous given that leaving involves network
// (which is not outrageous given that leaving involves network
// requests.)
const stateFeaturesBaseConference
= getState()['features/base/conference'];

View File

@@ -52,7 +52,7 @@ StateListenerRegistry.register(
/**
* Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant
* tiles accodingly.
* tiles accordingly.
*
* @param {Array<string>} oldScreenshareSourceNames - List of old screenshare source names.
* @param {Array<string>} newScreenshareSourceNames - Current list of screenshare source names.

View File

@@ -227,14 +227,14 @@ class MeetingsList extends Component<IProps> {
<Container
className = { rootClassName }
key = { index }
onClick = { onPress }>
onClick = { onPress }
tabIndex = { 0 }>
<Container className = 'right-column'>
<Text
className = 'title'
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
role = 'button'>
{ title }
</Text>
{

View File

@@ -41,8 +41,8 @@ export function formatURLText(text = '') {
}
if (!result) {
// This will be the case for invalid URLs or URLs without a host (emails for example). In this case beacuse
// of the issue with PunycodeJS that truncates parts of the text when there is '@' we split the text by '@'
// This will be the case for invalid URLs or URLs without a host (emails for example). In this case due to
// the issue with PunycodeJS that truncates parts of the text when there is '@' we split the text by '@'
// and use punycode for every separate part to prevent homograph attacks.
result = text.split('@').map(punycode.toASCII)
.join('@');
@@ -59,7 +59,7 @@ export function formatURLText(text = '') {
*/
export function getSupportUrl(stateful: IStateful) {
// TODO: Once overwriting trough interface config is completelly gone we should think of a way to be able to set
// TODO: Once overwriting through interface config is completely gone we should think of a way to be able to set
// the value in the branding and not return the default value from interface config.
return toState(stateful)['features/dynamic-branding'].supportUrl || interfaceConfig?.SUPPORT_URL;
}

View File

@@ -12,7 +12,7 @@ declare let __DEV__: any;
/**
* Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
* {@code true}), we persist the entire subtree. If it's an {@code Object}, we
* perist a filtered subtree based on the properties of the config object.
* persist a filtered subtree based on the properties of the config object.
*/
declare type ElementConfig = boolean | Object;

View File

@@ -5,7 +5,7 @@ import { setAspectRatio, setReducedUI } from './actions';
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio and
* Middleware that handles window dimension changes and updates the aspect ratio and
* reduced UI modes accordingly.
*
* @param {Store} store - The redux store.

View File

@@ -17,7 +17,7 @@ export * from './functions.any';
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {number|undefined} [options.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}

View File

@@ -31,7 +31,7 @@ export * from './functions.any';
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {number|undefined} [options.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.

View File

@@ -98,6 +98,9 @@ const useStyles = makeStyles()(theme => {
'&.error': {
boxShadow: `0px 0px 0px 2px ${theme.palette.textError}`
},
'&.clearable-input': {
paddingRight: '46px'
}
},
@@ -121,10 +124,6 @@ const useStyles = makeStyles()(theme => {
cursor: 'pointer'
},
clearableInput: {
paddingRight: '46px'
},
clearButton: {
position: 'absolute',
right: '16px',
@@ -185,6 +184,7 @@ const Input = React.forwardRef<any, IProps>(({
}: IProps, ref) => {
const { classes: styles, cx } = useStyles();
const isMobile = isMobileBrowser();
const showClearIcon = clearable && value !== '' && !disabled;
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
onChange?.(e.target.value), []);
@@ -211,7 +211,7 @@ const Input = React.forwardRef<any, IProps>(({
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
error && 'error', showClearIcon && 'clearable-input', icon && 'icon-input') }
disabled = { disabled }
id = { id }
maxLength = { maxLength }
@@ -232,7 +232,7 @@ const Input = React.forwardRef<any, IProps>(({
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
error && 'error', showClearIcon && 'clearable-input', icon && 'icon-input') }
data-testid = { testId }
disabled = { disabled }
id = { id }
@@ -252,7 +252,7 @@ const Input = React.forwardRef<any, IProps>(({
type = { type }
value = { value } />
)}
{clearable && !disabled && value !== '' && <button className = { styles.clearButton }>
{showClearIcon && <button className = { styles.clearButton }>
<Icon
onClick = { clearInput }
size = { 20 }

View File

@@ -19,12 +19,12 @@ interface ISelectProps {
className?: string;
/**
* Wether or not the select is disabled.
* Whether or not the select is disabled.
*/
disabled?: boolean;
/**
* Wether or not the select is in the error state.
* Whether or not the select is in the error state.
*/
error?: boolean;

View File

@@ -13,7 +13,7 @@ import { SECURITY_URL } from './contants';
*
* @param {IReduxState} state - The redux state.
* @param {Function} t - The translation function.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warining.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warning.
* @returns {Text}
*/
export default function getUnsafeRoomText(state: IReduxState, t: Function, context: 'meeting' | 'prejoin' | 'welcome') {

View File

@@ -6,7 +6,7 @@ import { SECURITY_URL } from './contants';
* Gets the unsafe room text for the given context.
*
* @param {Function} t - The translation function.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warining.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warning.
* @returns {string}
*/
export default function getUnsafeRoomText(t: Function, context: 'meeting' | 'prejoin' | 'welcome') {

View File

@@ -18,7 +18,7 @@ import CalendarListContent from './CalendarListContent.native';
import styles from './styles';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
* The type of the React {@code Component} props of {@link CalendarList}.
*/
interface IProps extends WithTranslation {

View File

@@ -152,7 +152,7 @@ export default createStyleSheet({
},
/**
* The container for all the lines if the norification.
* The container for all the lines if the notification.
*/
notificationTextContainer: {
flexDirection: 'column',

View File

@@ -1,5 +1,5 @@
import { Theme } from '@mui/material';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -264,7 +264,7 @@ const ChatMessage = ({
*
* @returns {React$Element<*>}
*/
const renderReactions = () => {
const renderReactions = useMemo(() => {
if (!message.reactions || message.reactions.size === 0) {
return null;
}
@@ -321,7 +321,7 @@ const ChatMessage = ({
</div>
</Popover>
);
};
}, [ message?.reactions, isHovered, isReactionsOpen ]);
return (
<div
@@ -366,7 +366,7 @@ const ChatMessage = ({
<div className = { classes.chatMessageFooterLeft }>
{message.reactions && message.reactions.size > 0 && (
<>
{renderReactions()}
{renderReactions}
</>
)}
</div>

View File

@@ -11,7 +11,7 @@ import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
*
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
* instance which initiated the kick event.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @param {?Function} submit - The function to execute after submitting the dialog.
* @returns {Function}
*/
export function notifyKickedOut(participant: any, submit?: Function) {
@@ -38,7 +38,7 @@ export function notifyKickedOut(participant: any, submit?: Function) {
* Notify that we've been kicked out of the conference.
*
* @param {string} reasonKey - The translation key for the reason why the conference failed.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @param {?Function} submit - The function to execute after submitting the dialog.
* @returns {Function}
*/
export function notifyConferenceFailed(reasonKey: string, submit?: Function) {

View File

@@ -99,7 +99,7 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
iAmVisitor: boolean;
/**
* Wether noise suppression is on or not.
* Whether noise suppression is on or not.
*/
noiseSuppressionEnabled: boolean;

View File

@@ -50,7 +50,7 @@ export function setMediaEncryptionKey(keyInfo: Object) {
}
/**
* Dispatches an action to start participant e2ee verficiation process.
* Dispatches an action to start participant e2ee verification process.
*
* @param {string} pId - The participant id.
* @returns {{
@@ -69,7 +69,7 @@ export function startVerification(pId: string) {
* Dispatches an action to set participant e2ee verification status.
*
* @param {string} pId - The participant id.
* @param {boolean} isVerified - The verifcation status.
* @param {boolean} isVerified - The verification status.
* @returns {{
* type: PARTICIPANT_VERIFIED,
* pId: string,

View File

@@ -57,7 +57,7 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
remoteParticipants.delete(speaker);
}
// Always update the order of the thumnails.
// Always update the order of the thubmnails.
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);

View File

@@ -254,7 +254,7 @@ export function getFilmstripDimensions({
/**
* Returns true if the local thumbnail should be displayed separately and false otherwise.
*
* @returns {boolean} - True if the local thumbnail should be displayed separately and flase otherwise.
* @returns {boolean} - True if the local thumbnail should be displayed separately and false otherwise.
*/
export function shouldDisplayLocalThumbnailSeparately() {
// XXX Our current design is to have the local participant separate from

View File

@@ -322,7 +322,7 @@ export function calculateResponsiveTileViewDimensions({
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants, desiredNumberOfVisibleTiles); c++) {
const r = Math.ceil(numberOfParticipants / c);
// we want to display as much as possible tumbnails up to desiredNumberOfVisibleTiles
// we want to display as much as possible thumbnails up to desiredNumberOfVisibleTiles
const visibleRows
= numberOfParticipants <= desiredNumberOfVisibleTiles ? r : Math.floor(desiredNumberOfVisibleTiles / c);

View File

@@ -84,7 +84,7 @@ export function updateKnownLargeVideoResolution(resolution: number) {
}
/**
* Sets the dimenstions of the large video in redux.
* Sets the dimensions of the large video in redux.
*
* @param {number} height - The height of the large video.
* @param {number} width - The width of the large video.

View File

@@ -9,7 +9,7 @@ import { isLocalVideoTrackDesktop } from '../../base/tracks/functions.any';
* Checks whether full-screen state should be used or not.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Whether full-screen state shuld be used or not.
* @returns {boolean} - Whether full-screen state should be used or not.
*/
export function shouldUseFullScreen(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];

View File

@@ -350,6 +350,7 @@ const Notification = ({
onClick = { onDismiss }
size = { 20 }
src = { IconCloseLarge }
tabIndex = { 0 }
testId = { `${titleKey || descriptionKey}-dismiss` } />
)}
</div>

View File

@@ -286,7 +286,7 @@ export const isMuteAllVisible = (state: IReduxState) => {
* Returns true if renaming the currently joined breakout room is allowed and false otherwise.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if reanming the currently joined breakout room is allowed and false otherwise.
* @returns {boolean} - True if renaming the currently joined breakout room is allowed and false otherwise.
*/
export function isCurrentRoomRenamable(state: IReduxState) {
return isInBreakoutRoom(state) && isBreakoutRoomRenameAllowed(state);

View File

@@ -31,7 +31,7 @@ StateListenerRegistry.register(
}
});
const parsePollData = (pollData: IPollData): IPoll | null => {
const parsePollData = (pollData: Partial<IPollData>): IPoll | null => {
if (typeof pollData !== 'object' || pollData === null) {
return null;
}
@@ -122,6 +122,18 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
case COMMAND_NEW_POLL: {
const { pollId, answers, senderId, question } = data;
const tmp = {
id: pollId,
answers,
question
};
// Check integrity of the poll data.
// TODO(saghul): we should move this to the server side, likely by storing the
// poll data in the room metadata.
if (parsePollData(tmp) === null) {
return;
}
const poll = {
changingVote: false,

View File

@@ -117,7 +117,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
return () => BackHandler.removeEventListener('hardwareBackPress', goBack);
}, []); // dispatch is not in the dependancy list because we want the action to be dispatched only once when
}, []); // dispatch is not in the dependency list because we want the action to be dispatched only once when
// the component is mounted.
const headerLeft = () => {

View File

@@ -216,11 +216,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
if (updatedSessionData?.status === ON) {
// We receive 2 updates of the session status ON. The first one is from jibri when it joins.
// The second one is from jicofo which will deliever the initiator value. Since the start
// The second one is from jicofo which will deliver the initiator value. Since the start
// recording notification uses the initiator value we skip the jibri update and show the
// notification on the update from jicofo.
// FIXE: simplify checks when the backend start sending only one status ON update containing the
// initiator.
// FIXME: simplify checks when the backend start sending only one status ON update containing
// the initiator.
if (initiator && !oldSessionData?.initiator) {
if (typeof recordingLimit === 'object') {
dispatch(showRecordingLimitNotification(mode));

View File

@@ -113,7 +113,7 @@ ReducerRegistry.register<IRecordingState>(STORE_NAME,
* @param {Array} sessionDatas - The current sessions in the redux store.
* @param {Object} newSessionData - The updated session data.
* @private
* @returns {Array} The session datas with the updated session data added.
* @returns {Array} The session data with the updated session data added.
*/
function _updateSessionDatas(sessionDatas: ISessionData[], newSessionData: ISessionData) {
const hasExistingSessionData = sessionDatas.find(
@@ -133,7 +133,7 @@ function _updateSessionDatas(sessionDatas: ISessionData[], newSessionData: ISess
});
} else {
// If the session data is not present, then there is nothing to update
// and instead it needs to be added to the known session datas.
// and instead it needs to be added to the known session data.
newSessionDatas = [
...sessionDatas,
{ ...newSessionData }

View File

@@ -107,7 +107,7 @@ export function requestRemoteControl(userId: string) {
dispatch(setRemoteControlActive(true));
logger.log(`Requsting remote control permissions from: ${userId}`);
logger.log(`Requesting remote control permissions from: ${userId}`);
const { conference } = state['features/base/conference'];

View File

@@ -79,7 +79,7 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
showPrejoinSettings: boolean;
/**
* Wether or not the stage filmstrip is enabled.
* Whether or not the stage filmstrip is enabled.
*/
stageFilmstripEnabled: boolean;
}

View File

@@ -25,7 +25,7 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
displayShortcuts: boolean;
/**
* Wether the keyboard shortcuts are enabled or not.
* Whether the keyboard shortcuts are enabled or not.
*/
keyboardShortcutsEnabled: boolean;

View File

@@ -28,17 +28,6 @@ export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
*/
export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO';
/**
* The type of the action which signals to disable or enable the shared video
* button.
*
* {
* type: SET_DISABLE_BUTTON
* }
*/
export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON';
/**
* The type of the action which sets an array of whitelisted urls.
*

View File

@@ -1 +0,0 @@
export * from './actions.any';

View File

@@ -1,19 +0,0 @@
import { SET_DISABLE_BUTTON } from './actionTypes';
export * from './actions.any';
/**
* Disabled share video button.
*
* @param {boolean} disabled - The current state of the share video button.
* @returns {{
* type: SET_DISABLE_BUTTON,
* disabled: boolean
* }}
*/
export function setDisableButton(disabled: boolean) {
return {
type: SET_DISABLE_BUTTON,
disabled
};
}

View File

@@ -5,7 +5,7 @@ import { IReduxState, IStore } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { getLocalParticipant } from '../../../base/participants/functions';
import { setSharedVideoStatus } from '../../actions.any';
import { setSharedVideoStatus } from '../../actions';
import { PLAYBACK_STATUSES } from '../../constants';
/**

View File

@@ -7,7 +7,7 @@ import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions.native';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
/**

View File

@@ -15,7 +15,7 @@ import { showWarningNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { dockToolbox } from '../../../toolbox/actions';
import { muteLocal } from '../../../video-menu/actions.any';
import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
import { setSharedVideoStatus, stopSharedVideo } from '../../actions';
import { PLAYBACK_STATUSES } from '../../constants';
const logger = Logger.getLogger(__filename);

View File

@@ -3,8 +3,9 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions.any';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -83,16 +84,14 @@ class SharedVideoButton extends AbstractButton<IProps> {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const {
disabled: sharedVideoBtnDisabled,
status: sharedVideoStatus
} = state['features/shared-video'];
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const isSharing = isSharingStatus(sharedVideoStatus ?? '');
return {
_isDisabled: Boolean(sharedVideoBtnDisabled),
_sharingVideo: isSharingStatus(sharedVideoStatus ?? '')
_isDisabled: isSharing && ownerId !== localParticipantId,
_sharingVideo: isSharing
};
}
export default translate(connect(_mapStateToProps)(SharedVideoButton));

View File

@@ -1 +0,0 @@
import './middleware.any';

View File

@@ -22,7 +22,7 @@ import {
setAllowedUrlDomians,
setSharedVideoStatus,
showConfirmPlayingDialog
} from './actions.any';
} from './actions';
import {
DEFAULT_ALLOWED_URL_DOMAINS,
PLAYBACK_START,
@@ -55,27 +55,41 @@ MiddlewareRegistry.register(store => next => action => {
conference.addCommandListener(SHARED_VIDEO,
({ value, attributes }: { attributes: {
from: string; muted: string; state: string; time: string; }; value: string; }) => {
muted: string; state: string; time: string; }; value: string; },
from: string) => {
const state = getState();
const { from } = attributes;
const sharedVideoStatus = attributes.state;
const { ownerId } = state['features/shared-video'];
if (ownerId && ownerId !== from) {
logger.warn(
`User with id: ${from} sent shared video command: ${sharedVideoStatus} while we are playing.`);
return;
}
if (isSharingStatus(sharedVideoStatus)) {
// confirmShowVideo is undefined the first time we receive
// when confirmShowVideo is false we ignore everything except stop that resets it
if (getState()['features/shared-video'].confirmShowVideo === false) {
if (state['features/shared-video'].confirmShowVideo === false) {
return;
}
if (isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)
if (isURLAllowedForSharedVideo(value, state['features/shared-video'].allowedUrlDomains, true)
|| localParticipantId === from
|| getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
handleSharingVideoStatus(store, value, attributes, conference);
|| state['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
handleSharingVideoStatus(store, value, {
...attributes,
from
}, conference);
} else {
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(getState(), from), () => {
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(state, from), () => {
handleSharingVideoStatus(store, value, attributes, conference);
handleSharingVideoStatus(store, value, {
...attributes,
from
}, conference);
return true; // on mobile this is used to close the dialog
}));
@@ -87,11 +101,11 @@ MiddlewareRegistry.register(store => next => action => {
if (sharedVideoStatus === 'stop') {
const videoParticipant = getParticipantById(state, value);
if (getState()['features/shared-video'].confirmShowVideo === false) {
if (state['features/shared-video'].confirmShowVideo === false) {
dispatch(showWarningNotification({
titleKey: 'dialog.shareVideoLinkStopped',
titleArguments: {
name: getParticipantDisplayName(getState(), from)
name: getParticipantDisplayName(state, from)
}
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}

View File

@@ -1,41 +0,0 @@
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { getLocalParticipant } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { setDisableButton } from './actions.web';
import { PLAYBACK_STATUSES, SHARED_VIDEO } from './constants';
import { isSharedVideoEnabled } from './functions';
import './middleware.any';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const state = getState();
const localParticipantId = getLocalParticipant(state)?.id;
switch (action.type) {
case CONFERENCE_JOIN_IN_PROGRESS: {
if (!isSharedVideoEnabled(state)) {
break;
}
const { conference } = action;
conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes:
{ from: string; state: string; }; }) => {
const { from } = attributes;
const status = attributes.state;
if (status === PLAYBACK_STATUSES.PLAYING) {
if (localParticipantId !== from) {
dispatch(setDisableButton(true));
}
} else if (status === 'stop') {
dispatch(setDisableButton(false));
}
});
break;
}
}
return next(action);
});

View File

@@ -4,7 +4,6 @@ import {
RESET_SHARED_VIDEO_STATUS,
SET_ALLOWED_URL_DOMAINS,
SET_CONFIRM_SHOW_VIDEO,
SET_DISABLE_BUTTON,
SET_SHARED_VIDEO_STATUS
} from './actionTypes';
import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants';
@@ -16,7 +15,6 @@ const initialState = {
export interface ISharedVideoState {
allowedUrlDomains: Array<string>;
confirmShowVideo?: boolean;
disabled?: boolean;
muted?: boolean;
ownerId?: string;
status?: string;
@@ -30,7 +28,7 @@ export interface ISharedVideoState {
*/
ReducerRegistry.register<ISharedVideoState>('features/shared-video',
(state = initialState, action): ISharedVideoState => {
const { videoUrl, status, time, ownerId, disabled, muted, volume } = action;
const { videoUrl, status, time, ownerId, muted, volume } = action;
switch (action.type) {
case RESET_SHARED_VIDEO_STATUS:
@@ -55,12 +53,6 @@ ReducerRegistry.register<ISharedVideoState>('features/shared-video',
volume
};
case SET_DISABLE_BUTTON:
return {
...state,
disabled
};
case SET_ALLOWED_URL_DOMAINS: {
return {
...state,

View File

@@ -202,7 +202,7 @@ export function getVisibleButtons({
setButtonsNotifyClickMode(allButtons, buttonsWithNotifyClick);
const filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be comming from config.mainToolbarButtons
typeof key !== 'undefined' // filter invalid buttons that may be coming from config.mainToolbarButtons
// override
&& !jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, toolbarButtons));
@@ -264,7 +264,7 @@ interface ICSSTransitionObject {
* @returns {ICSSTransitionObject}
*/
export function getTransitionParamsForElementsAboveToolbox(isToolbarVisible: boolean): ICSSTransitionObject {
// The transistion time and delay is different to account for the time when the toolbar is about to hide/show but
// The transition time and delay is different to account for the time when the toolbar is about to hide/show but
// the elements don't have to move.
return isToolbarVisible ? {
duration: 0.15,

View File

@@ -142,7 +142,7 @@ function _setFullScreen(next: Function, action: AnyAction) {
/**
* Common logic to gather buttons that have to notify the api when clicked.
*
* @param {Array} buttonsWithNotifyClick - The array of systme buttons that need to notify the api.
* @param {Array} buttonsWithNotifyClick - The array of system buttons that need to notify the api.
* @param {Array} customButtons - The custom buttons.
* @returns {Array}
*/

View File

@@ -9,7 +9,7 @@ import { isWhiteboardParticipant } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { stopSharedVideo } from '../../../shared-video/actions.any';
import { stopSharedVideo } from '../../../shared-video/actions';
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
import { setWhiteboardOpen } from '../../../whiteboard/actions';

View File

@@ -29,7 +29,7 @@ export function setMaxReceiverVideoQualityForLargeVideo(maxReceiverVideoQuality:
}
/**
* Sets the max frame height that should be received for the screen sharing filmstrip particpant.
* Sets the max frame height that should be received for the screen sharing filmstrip participant.
*
* @param {number} maxReceiverVideoQuality - The max video frame height to
* receive.

View File

@@ -309,7 +309,7 @@ StateListenerRegistry.register(
});
/**
* Returns the source names asociated with the given participants list.
* Returns the source names associated with the given participants list.
*
* @param {Array<string>} participantList - The list of participants.
* @param {Object} state - The redux state.

View File

@@ -41,7 +41,7 @@ module:hook("pre-iq/full", function(event)
session.granted_jitsi_meet_context_features,
occupant.role == 'moderator');
if jibri.attr.action == 'start' then
if jibri.attr.action == 'start' or jibri.attr.action == 'stop' then
if not is_allowed then
module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza));
session.send(st.error_reply(stanza, 'auth', 'forbidden'));

View File

@@ -80,7 +80,7 @@ function Util.new(module)
These setups relay on configuration 'muc_domain_base' which holds
the main domain and we use it to subtract subdomains from the
virtual addresses.
The following confgurations are for multidomain setups and domain name
The following configurations are for multidomain setups and domain name
verification:
--]]
@@ -225,7 +225,7 @@ function Util:get_public_key(keyId)
self.cache:set(keyId, content);
else
if code == nil then
-- this is timout after nr_retries retries
-- this is timeout after nr_retries retries
module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl);
end
end

View File

@@ -1,9 +1,12 @@
/* global APP $ */
import { multiremotebrowser } from '@wdio/globals';
import { Key } from 'webdriverio';
import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import BreakoutRooms from '../pageobjects/BreakoutRooms';
import ChatPanel from '../pageobjects/ChatPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import Notifications from '../pageobjects/Notifications';
@@ -120,7 +123,10 @@ export class Participant {
async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
const config = {
room: ctx.roomName,
configOverwrite: this.config,
configOverwrite: {
...this.config,
...options.configOverwrite || {}
},
interfaceConfigOverwrite: {
SHOW_CHROME_EXTENSION_BANNER: false
}
@@ -129,7 +135,7 @@ export class Participant {
if (!options.skipDisplayName) {
// @ts-ignore
config.userInfo = {
displayName: this._name
displayName: options.displayName || this._name
};
}
@@ -251,6 +257,22 @@ export class Participant {
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
/**
* Checks if the meeting supports breakout rooms.
*/
async isBreakoutRoomsSupported() {
return await this.driver.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
}
/**
* Checks if the participant is in breakout room.
*/
async isInBreakoutRoom() {
return await this.driver.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
}
/**
* Waits to join the muc.
*
@@ -321,6 +343,22 @@ export class Participant {
});
}
/**
* Returns the chat panel for this participant.
*/
getChatPanel(): ChatPanel {
return new ChatPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*
* @returns {BreakoutRooms}
*/
getBreakoutRooms(): BreakoutRooms {
return new BreakoutRooms(this);
}
/**
* Returns the toolbar for this participant.
*
@@ -404,16 +442,40 @@ export class Participant {
}
/**
* Returns the local display name.
* Returns the local display name element.
* @private
*/
async getLocalDisplayName() {
private async getLocalDisplayNameElement() {
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]');
return localVideoContainer.$('span[id="localDisplayName"]');
}
return await localDisplayName.getText();
/**
* Returns the local display name.
*/
async getLocalDisplayName() {
return await (await this.getLocalDisplayNameElement()).getText();
}
/**
* Sets the display name of the local participant.
*/
async setLocalDisplayName(displayName: string) {
const localDisplayName = await this.getLocalDisplayNameElement();
await localDisplayName.click();
await this.driver.keys(displayName);
await this.driver.keys(Key.Return);
// just click somewhere to lose focus, to make sure editing has ended
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
await localVideoContainer.click();
}
/**

View File

@@ -6,6 +6,8 @@ import { v4 as uuidv4 } from 'uuid';
import { Participant } from './Participant';
import { IContext, IJoinOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
/**
* Ensure that there is on participant.
*
@@ -14,38 +16,81 @@ import { IContext, IJoinOptions } from './types';
* @returns {Promise<void>}
*/
export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
ctx.p1 = new Participant('participant1');
await ctx.p1.joinConference(ctx, {
...options,
skipInMeetingChecks: true
});
await joinTheModeratorAsP1(ctx, options);
}
/**
* Ensure that there are three participants.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
await joinTheModeratorAsP1(ctx);
const p2 = new Participant('participant2');
const p3 = new Participant('participant3');
ctx.p2 = p2;
ctx.p3 = p3;
export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
// these need to be all, so we get the error when one fails
await Promise.all([
p2.joinConference(ctx),
p3.joinConference(ctx)
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
...options
})
]);
const { skipInMeetingChecks } = options;
await Promise.all([
p2.waitForRemoteStreams(2),
p3.waitForRemoteStreams(2)
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(2),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(2)
]);
}
/**
* Ensure that there are four participants.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export async function ensureFourParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
// these need to be all, so we get the error when one fails
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
...options
}),
_joinParticipant('participant4', ctx.p4, p => {
ctx.p4 = p;
}, {
displayName: 'p4',
...options
})
]);
const { skipInMeetingChecks } = options;
await Promise.all([
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3)
]);
}
@@ -57,7 +102,7 @@ export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
* @returns {Promise<void>}
*/
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = 'participant1';
const p1DisplayName = 'p1';
let token;
// if it is jaas create the first one to be moderator and second not moderator
@@ -66,9 +111,10 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
}
// make sure the first participant is moderator, if supported by deployment
await _joinParticipant(p1DisplayName, ctx.p1, p => {
await _joinParticipant('participant1', ctx.p1, p => {
ctx.p1 = p;
}, {
displayName: p1DisplayName,
...options,
skipInMeetingChecks: true
}, token);
@@ -88,7 +134,10 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, options),
}, {
displayName: 'p2',
...options
}),
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
]);
@@ -237,3 +286,19 @@ export function parseJid(str: string): {
resource: domainParts.length > 0 ? domainParts[1] : undefined
};
}
/**
* Check the subject of the participant.
* @param participant
* @param subject
*/
export async function checkSubject(participant: Participant, subject: string) {
const localTile = participant.driver.$(SUBJECT_XPATH);
await localTile.waitForExist();
await localTile.moveTo();
const txt = await localTile.getText();
expect(txt.startsWith(subject)).toBe(true);
}

View File

@@ -1,3 +1,5 @@
import { IConfig } from '../../react/features/base/config/configType';
import type { Participant } from './Participant';
import WebhookProxy from './WebhookProxy';
@@ -17,6 +19,16 @@ export type IContext = {
export type IJoinOptions = {
/**
* Config overwrites to use.
*/
configOverwrite?: IConfig;
/**
* The display name to use.
*/
displayName?: string;
/**
* Whether to skip setting display name.
*/

View File

@@ -1,4 +1,4 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const START_AUDIO_MODERATION = 'participants-pane-context-menu-start-audio-moderation';
const STOP_AUDIO_MODERATION = 'participants-pane-context-menu-stop-audio-moderation';
@@ -8,17 +8,7 @@ const STOP_VIDEO_MODERATION = 'participants-pane-context-menu-stop-video-moderat
/**
* Represents the Audio Video Moderation menu in the participants pane.
*/
export default class AVModerationMenu {
private participant: Participant;
/**
* Represents the Audio Video Moderation menu in the participants pane.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class AVModerationMenu extends BasePageObject {
/**
* Clicks the start audio moderation menu item.
*/

View File

@@ -1,4 +1,4 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const CLOSE_BUTTON = 'modal-header-close-button';
const OK_BUTTON = 'modal-dialog-ok-button';
@@ -6,18 +6,7 @@ const OK_BUTTON = 'modal-dialog-ok-button';
/**
* Base class for all dialogs.
*/
export default class BaseDialog {
participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class BaseDialog extends BasePageObject {
/**
* Clicks on the X (close) button.
*/

View File

@@ -0,0 +1,16 @@
import { Participant } from '../helpers/Participant';
/**
* Represents the base page object.
* All page object has the current participant (holding the driver/browser session).
*/
export default class BasePageObject {
participant: Participant;
/**
* Represents the base page object.
*/
constructor(participant: Participant) {
this.participant = participant;
}
}

View File

@@ -0,0 +1,230 @@
import { Participant } from '../helpers/Participant';
import BaseDialog from './BaseDialog';
import BasePageObject from './BasePageObject';
const BREAKOUT_ROOMS_CLASS = 'breakout-room-container';
const ADD_BREAKOUT_ROOM = 'Add breakout room';
const MORE_LABEL = 'More';
const LEAVE_ROOM_LABEL = 'Leave breakout room';
const AUTO_ASSIGN_LABEL = 'Auto assign to breakout rooms';
/**
* Represents a single breakout room and the operations for it.
*/
class BreakoutRoom extends BasePageObject {
title: string;
id: string;
count: number;
/**
* Constructs a breakout room.
*/
constructor(participant: Participant, title: string, id: string) {
super(participant);
this.title = title;
this.id = id;
const tMatch = title.match(/.*\((.*)\)/);
if (tMatch) {
this.count = parseInt(tMatch[1], 10);
}
}
/**
* Returns room name.
*/
get name() {
return this.title.split('(')[0].trim();
}
/**
* Returns the number of participants in the room.
*/
get participantCount() {
return this.count;
}
/**
* Collapses the breakout room.
*/
async collapse() {
const collapseElem = this.participant.driver.$(
`div[data-testid="${this.id}"]`);
await collapseElem.click();
}
/**
* Joins the breakout room.
*/
async joinRoom() {
const joinButton = this.participant.driver
.$(`button[data-testid="join-room-${this.id}"]`);
await joinButton.waitForClickable();
await joinButton.click();
}
/**
* Removes the breakout room.
*/
async removeRoom() {
await this.openContextMenu();
const removeButton = this.participant.driver.$(`#remove-room-${this.id}`);
await removeButton.waitForClickable();
await removeButton.click();
}
/**
* Renames the breakout room.
*/
async renameRoom(newName: string) {
await this.openContextMenu();
const renameButton = this.participant.driver.$(`#rename-room-${this.id}`);
await renameButton.click();
const newNameInput = this.participant.driver.$('input[name="breakoutRoomName"]');
await newNameInput.waitForStable();
await newNameInput.setValue(newName);
await new BaseDialog(this.participant).clickOkButton();
}
/**
* Closes the breakout room.
*/
async closeRoom() {
await this.openContextMenu();
const closeButton = this.participant.driver.$(`#close-room-${this.id}`);
await closeButton.waitForClickable();
await closeButton.click();
}
/**
* Opens the context menu.
* @private
*/
private async openContextMenu() {
const listItem = this.participant.driver.$(`div[data-testid="${this.id}"]`);
await listItem.click();
const button = listItem.$(`aria/${MORE_LABEL}`);
await button.waitForClickable();
await button.click();
}
}
/**
* All breakout rooms objects and operations.
*/
export default class BreakoutRooms extends BasePageObject {
/**
* Returns the number of breakout rooms.
*/
async getRoomsCount() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
return await this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`).length;
}
/**
* Adds a breakout room.
*/
async addBreakoutRoom() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
await addBreakoutButton.waitForDisplayed();
await addBreakoutButton.click();
}
/**
* Returns all breakout rooms.
*/
async getRooms(): Promise<BreakoutRoom[]> {
const rooms = this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`);
return rooms.map(async room => new BreakoutRoom(
this.participant, await room.$('span').getText(), await room.getAttribute('data-testid')));
}
/**
* Leave by clicking the leave button in participant pane.
*/
async leaveBreakoutRoom() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
await leaveButton.isClickable();
await leaveButton.click();
}
/**
* Auto assign participants to breakout rooms.
*/
async autoAssignToBreakoutRooms() {
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
await button.waitForClickable();
await button.click();
}
/**
* Tries to send a participant to a breakout room.
*/
async sendParticipantToBreakoutRoom(participant: Participant, roomName: string) {
const participantsPane = this.participant.getParticipantsPane();
await participantsPane.selectParticipant(participant);
await participantsPane.openParticipantContextMenu(participant);
const sendButton = this.participant.driver.$(`aria/${roomName}`);
await sendButton.waitForClickable();
await sendButton.click();
}
// /**
// * Open context menu for given participant.
// */
// async openParticipantContextMenu(participant: Participant) {
// const listItem = this.participant.driver.$(
// `div[@id="participant-item-${await participant.getEndpointId()}"]`);
//
// await listItem.waitForDisplayed();
// await listItem.moveTo();
//
// const button = listItem.$(`aria/${PARTICIPANT_MORE_LABEL}`);
//
// await button.waitForClickable();
// await button.click();
// }
}

View File

@@ -0,0 +1,22 @@
import BasePageObject from './BasePageObject';
/**
* Chat panel elements.
*/
export default class ChatPanel extends BasePageObject {
/**
* Is chat panel open.
*/
async isOpen() {
return await this.participant.driver.$('#sideToolbarContainer').isExisting();
}
/**
* Presses the "chat" keyboard shortcut which opens or closes the chat
* panel.
*/
async pressShortcut() {
await this.participant.driver.$('body').click();
await this.participant.driver.keys([ 'c' ]);
}
}

View File

@@ -1,22 +1,17 @@
import { Participant } from '../helpers/Participant';
import BaseDialog from './BaseDialog';
import BasePageObject from './BasePageObject';
const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
const LOCAL_USER_CONTROLS = 'aria/Local user controls';
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';
/**
* Filmstrip elements.
*/
export default class Filmstrip {
private participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Filmstrip extends BasePageObject {
/**
* Asserts that {@code participant} shows or doesn't show the audio
* mute icon for the conference participant identified by
@@ -131,4 +126,31 @@ export default class Filmstrip {
async muteVideo(participant: Participant) {
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
}
/**
* Clicks on the hide self view button from local video.
*/
async hideSelfView() {
// open local video menu
await this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();
// click Hide self view button
const hideSelfViewButton = this.participant.driver.$(HIDE_SELF_VIEW_BUTTON_XPATH);
await hideSelfViewButton.waitForExist();
await hideSelfViewButton.waitForClickable();
await hideSelfViewButton.click();
}
/**
* Checks whether the local self view is displayed or not.
*/
async assertSelfViewIsHidden(hidden: boolean) {
await this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
reverse: hidden,
timeout: 5000,
timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
});
}
}

View File

@@ -1,20 +1,11 @@
import { Participant } from '../helpers/Participant';
import { LOG_PREFIX } from '../helpers/browserLogger';
import BasePageObject from './BasePageObject';
/**
* The Iframe API and helpers from iframeAPITest.html
*/
export default class IframeAPI {
private participant: Participant;
/**
* Initializes for a participant.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class IframeAPI extends BasePageObject {
/**
* Returns the json object from the iframeAPI helper.
* @param event

View File

@@ -1,25 +1,17 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
const REENABLE_SELF_VIEW_NOTIFICATION_ID = 'notify.selfViewTitle';
const REENABLE_SELF_VIEW_CLOSE_NOTIFICATION = 'notify.selfViewTitle-dismiss';
/**
* Gathers all notifications logic in the UI and obtaining those.
*/
export default class Notifications {
private participant: Participant;
/**
* Represents the Audio Video Moderation menu in the participants pane.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Notifications extends BasePageObject {
/**
* Waits for the raised hand notification to be displayed.
* The notification on moderators page when the participant tries to unmute.
@@ -51,4 +43,23 @@ export default class Notifications {
[ `${JOIN_ONE_TEST_ID}-dismiss`, `${JOIN_TWO_TEST_ID}-dismiss`, `${JOIN_MULTIPLE_TEST_ID}-dismiss` ]
.map(async id => this.participant.driver.$(`#${id}"]`).click()));
}
/**
* Waits for the self view notification to be displayed.
*/
async waitForReEnableSelfViewNotification() {
const el
= this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_NOTIFICATION_ID}"]`);
await el.waitForExist({ timeout: 2000 });
await el.waitForDisplayed();
}
/**
* Closes the self view notification.
*/
async closeReEnableSelfViewNotification() {
await this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_CLOSE_NOTIFICATION}"]`).click();
}
}

View File

@@ -1,6 +1,7 @@
import { Participant } from '../helpers/Participant';
import AVModerationMenu from './AVModerationMenu';
import BasePageObject from './BasePageObject';
/**
* Classname of the closed/hidden participants pane
@@ -10,18 +11,7 @@ const PARTICIPANTS_PANE = 'participants_pane';
/**
* Represents the participants pane from the UI.
*/
export default class ParticipantsPane {
private participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class ParticipantsPane extends BasePageObject {
/**
* Gets the audio video moderation menu.
*/
@@ -138,22 +128,10 @@ export default class ParticipantsPane {
await this.participant.getNotifications().dismissAnyJoinNotification();
const participantId = await participantToUnmute.getEndpointId();
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
await participantItem.waitForExist();
await participantItem.waitForStable();
await participantItem.waitForDisplayed();
await participantItem.moveTo();
await this.selectParticipant(participantToUnmute);
if (fromContextMenu) {
const meetingParticipantMoreOptions = this.participant.driver
.$(`[data-testid="participant-more-options-${participantId}"]`);
await meetingParticipantMoreOptions.waitForExist();
await meetingParticipantMoreOptions.waitForDisplayed();
await meetingParticipantMoreOptions.waitForStable();
await meetingParticipantMoreOptions.moveTo();
await meetingParticipantMoreOptions.click();
await this.openParticipantContextMenu(participantToUnmute);
}
const unmuteButton = this.participant.driver
@@ -162,4 +140,32 @@ export default class ParticipantsPane {
await unmuteButton.waitForExist();
await unmuteButton.click();
}
/**
* Open context menu for given participant.
*/
async selectParticipant(participant: Participant) {
const participantId = await participant.getEndpointId();
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
await participantItem.waitForExist();
await participantItem.waitForStable();
await participantItem.waitForDisplayed();
await participantItem.moveTo();
}
/**
* Open context menu for given participant.
*/
async openParticipantContextMenu(participant: Participant) {
const participantId = await participant.getEndpointId();
const meetingParticipantMoreOptions = this.participant.driver
.$(`[data-testid="participant-more-options-${participantId}"]`);
await meetingParticipantMoreOptions.waitForExist();
await meetingParticipantMoreOptions.waitForDisplayed();
await meetingParticipantMoreOptions.waitForStable();
await meetingParticipantMoreOptions.moveTo();
await meetingParticipantMoreOptions.click();
}
}

View File

@@ -1,7 +1,9 @@
import BaseDialog from './BaseDialog';
const EMAIL_FIELD = '#setEmail';
const HIDE_SELF_VIEW_CHECKBOX = '//input[@name="hide-self-view"]';
const SETTINGS_DIALOG_CONTENT = '.settings-pane';
const X_PATH_MORE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="General"]';
const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]';
/**
@@ -34,6 +36,13 @@ export default class SettingsDialog extends BaseDialog {
await this.openTab(X_PATH_PROFILE_TAB);
}
/**
* Selects the Profile tab to be displayed.
*/
async openMoreTab() {
await this.openTab(X_PATH_MORE_TAB);
}
/**
* Enters the passed in email into the email field.
* @param email
@@ -59,4 +68,21 @@ export default class SettingsDialog extends BaseDialog {
async submit() {
await this.clickOkButton();
}
/**
* Sets the state checked/selected of a checkbox in the settings dialog.
*/
async setHideSelfView(hideSelfView: boolean) {
await this.openMoreTab();
const checkbox = this.participant.driver.$(HIDE_SELF_VIEW_CHECKBOX);
await checkbox.waitForExist();
if (hideSelfView !== await checkbox.isSelected()) {
// we show a div with svg and text after the input and those elements grab the click
// so we need to click on the parent element
await this.participant.driver.$(`${HIDE_SELF_VIEW_CHECKBOX}//ancestor::div[1]`).click();
}
}
}

View File

@@ -1,14 +1,19 @@
// eslint-disable-next-line no-unused-vars
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const AUDIO_MUTE = 'Mute microphone';
const AUDIO_UNMUTE = 'Unmute microphone';
const CHAT = 'Open chat';
const CLOSE_CHAT = 'Close chat';
const CLOSE_PARTICIPANTS_PANE = 'Close participants pane';
const HANGUP = 'Leave the meeting';
const OVERFLOW_MENU = 'More actions menu';
const OVERFLOW = 'More actions';
const PARTICIPANTS = 'Open participants pane';
const PROFILE = 'Edit your profile';
const RAISE_HAND = 'Raise your hand';
const SETTINGS = 'Open settings';
const ENTER_TILE_VIEW_BUTTON = 'Enter tile view';
const EXIT_TILE_VIEW_BUTTON = 'Exit tile view';
const VIDEO_QUALITY = 'Manage video quality';
const VIDEO_MUTE = 'Stop camera';
const VIDEO_UNMUTE = 'Start camera';
@@ -16,18 +21,7 @@ const VIDEO_UNMUTE = 'Start camera';
/**
* The toolbar elements.
*/
export default class Toolbar {
private participant: Participant;
/**
* Creates toolbar for a participant.
*
* @param {Participant} participant - The participants.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Toolbar extends BasePageObject {
/**
* Returns the button.
*
@@ -36,7 +30,7 @@ export default class Toolbar {
* @private
*/
private getButton(accessibilityCSSSelector: string) {
return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
}
/**
@@ -125,7 +119,10 @@ export default class Toolbar {
*/
async clickParticipantsPaneButton(): Promise<void> {
this.participant.log('Clicking on: Participants pane Button');
await this.getButton(PARTICIPANTS).click();
// Special case for participants pane button, as it contains the number of participants and its label
// is changing
await this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
}
/**
@@ -151,6 +148,51 @@ export default class Toolbar {
await this.getButton(RAISE_HAND).click();
}
/**
* Clicks on the chat button that opens chat panel.
*/
async clickChatButton(): Promise<void> {
this.participant.log('Clicking on: Chat Button');
await this.getButton(CHAT).click();
}
/**
* Clicks on the chat button that closes chat panel.
*/
async clickCloseChatButton(): Promise<void> {
this.participant.log('Clicking on: Close Chat Button');
await this.getButton(CLOSE_CHAT).click();
}
/**
* Clicks on the tile view button which enables tile layout.
*/
async clickEnterTileViewButton() {
await this.getButton(ENTER_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the tile view button which exits tile layout.
*/
async clickExitTileViewButton() {
await this.getButton(EXIT_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the hangup button that ends the conference.
*/
async clickHangupButton(): Promise<void> {
this.participant.log('Clicking on: Hangup Button');
await this.getButton(HANGUP).click();
}
/**
* Clicks on the settings toolbar button which opens or closes the settings panel.
*/
async clickSettingsButton() {
await this.clickButtonInOverflowMenu(SETTINGS);
}
/**
* Ensure the overflow menu is open and clicks on a specified button.
* @param accessibilityLabel The accessibility label of the button to be clicked.
@@ -159,6 +201,10 @@ export default class Toolbar {
private async clickButtonInOverflowMenu(accessibilityLabel: string) {
await this.openOverflowMenu();
// sometimes the overflow button tooltip is over the last entry in the menu,
// so let's move focus away before clicking the button
await this.participant.driver.$('#overflow-context-menu').moveTo();
this.participant.log(`Clicking on: ${accessibilityLabel}`);
await this.getButton(accessibilityLabel).click();
@@ -170,7 +216,7 @@ export default class Toolbar {
* @private
*/
private async isOverflowMenuOpen() {
return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0;
return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
}
/**
@@ -215,7 +261,7 @@ export default class Toolbar {
* @private
*/
private async waitForOverFlowMenu(visible: boolean) {
await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({
await this.getButton(OVERFLOW_MENU).waitForDisplayed({
reverse: !visible,
timeout: 3000,
timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`

View File

@@ -33,36 +33,6 @@ describe('Audio only - ', () => {
await setAudioOnlyAndCheck(false);
});
/**
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
* p2 participant sees a video mute state for the former.
* @param enable
*/
async function setAudioOnlyAndCheck(enable: boolean) {
const { p1 } = ctx;
await p1.getVideoQualityDialog().setVideoQuality(enable);
await verifyVideoMute(enable);
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
.waitForDisplayed({ reverse: !enable });
}
/**
* Verifies that p1 and p2 see p1 as video muted or not.
* @param muted
*/
async function verifyVideoMute(muted: boolean) {
const { p1, p2 } = ctx;
// Verify the observer sees the testee in the desired muted state.
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
// Verify the testee sees itself in the desired muted state.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
}
/**
* Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
* as video muted.
@@ -92,3 +62,33 @@ describe('Audio only - ', () => {
await verifyVideoMute(false);
});
});
/**
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
* p2 participant sees a video mute state for the former.
* @param enable
*/
async function setAudioOnlyAndCheck(enable: boolean) {
const { p1 } = ctx;
await p1.getVideoQualityDialog().setVideoQuality(enable);
await verifyVideoMute(enable);
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
.waitForDisplayed({ reverse: !enable });
}
/**
* Verifies that p1 and p2 see p1 as video muted or not.
* @param muted
*/
async function verifyVideoMute(muted: boolean) {
const { p1, p2 } = ctx;
// Verify the observer sees the testee in the desired muted state.
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
// Verify the testee sees itself in the desired muted state.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
}

View File

@@ -0,0 +1,45 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('DisplayName - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx, {
skipDisplayName: true
});
});
it('check change', async () => {
const { p1, p2 } = ctx;
// default remote display name
const defaultDisplayName = await p1.driver.execute(() => config.defaultRemoteDisplayName);
const p1EndpointId = await p1.getEndpointId();
const p2EndpointId = await p2.getEndpointId();
// Checks whether default display names are set and shown, when both sides still miss the display name.
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(defaultDisplayName);
expect(await p2.getFilmstrip().getRemoteDisplayName(p1EndpointId)).toBe(defaultDisplayName);
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
await p2.setLocalDisplayName(randomName);
expect(await p2.getLocalDisplayName()).toBe(randomName);
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(randomName);
});
it('check persistence', async () => {
const { p2 } = ctx;
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
await p2.setLocalDisplayName(randomName);
expect(await p2.getLocalDisplayName()).toBe(randomName);
await p2.hangup();
await ensureTwoParticipants(ctx, {
skipDisplayName: true
});
expect(await p2.getLocalDisplayName()).toBe(randomName);
});
});

View File

@@ -0,0 +1,22 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('End Conference - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
it('hangup call and check', async () => {
const { p1 } = ctx;
const url = await p1.driver.getUrl();
await p1.getToolbar().clickHangupButton();
await p1.driver.waitUntil(
async () => await p1.driver.getUrl() !== url,
{
timeout: 5000,
timeoutMsg: 'p1 did not navigate away from the conference'
}
);
});
});

View File

@@ -0,0 +1,70 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Self view - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
it('hide from menu', async () => {
const { p1 } = ctx;
await checkSelfViewHidden(p1, false);
await p1.getFilmstrip().hideSelfView();
await checkSelfViewHidden(p1, true, true);
await p1.getToolbar().clickEnterTileViewButton();
await checkSelfViewHidden(p1, true);
});
it('show from settings', async () => {
const { p1 } = ctx;
await toggleSelfViewFromSettings(p1, false);
await checkSelfViewHidden(p1, false);
});
it('hide from settings', async () => {
const { p1 } = ctx;
await toggleSelfViewFromSettings(p1, true);
await checkSelfViewHidden(p1, true, true);
});
it('check in alone meeting', async () => {
const { p1, p2 } = ctx;
await checkSelfViewHidden(p1, true);
await p2.hangup();
await checkSelfViewHidden(p1, true);
});
});
/**
* Toggles the self view option from the settings dialog.
*/
async function toggleSelfViewFromSettings(participant: Participant, hide: boolean) {
await participant.getToolbar().clickSettingsButton();
const settings = participant.getSettingsDialog();
await settings.waitForDisplay();
await settings.setHideSelfView(hide);
await settings.submit();
}
/**
* Checks whether the local self view is displayed or not.
*/
async function checkSelfViewHidden(participant: Participant, hidden: boolean, checkNotification = false) {
if (checkNotification) {
await participant.getNotifications().waitForReEnableSelfViewNotification();
await participant.getNotifications().closeReEnableSelfViewNotification();
}
await participant.getFilmstrip().assertSelfViewIsHidden(hidden);
}

View File

@@ -0,0 +1,464 @@
import type { ChainablePromiseElement } from 'webdriverio';
import type { Participant } from '../../helpers/Participant';
import { checkSubject, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
const MAIN_ROOM_NAME = 'Main room';
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
const LIST_ITEM_CONTAINER = 'list-item-container';
describe('BreakoutRooms ', () => {
it('check support', async () => {
await ensureTwoParticipants(ctx);
if (!await ctx.p1.isBreakoutRoomsSupported()) {
ctx.skipSuiteTests = true;
}
});
it('add breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be no breakout rooms initially, list is sent with a small delay
await p1.driver.pause(2000);
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
// add one breakout room
await p1BreakoutRooms.addBreakoutRoom();
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 2000,
timeoutMsg: 'No breakout room added for p1'
});
// second participant should also see one breakout room
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
timeout: 2000,
timeoutMsg: 'No breakout room seen by p2'
});
});
it('join breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be one breakout room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 1000,
timeoutMsg: 'No breakout room seen by p1'
});
const roomsList = await p1BreakoutRooms.getRooms();
expect(roomsList.length).toBe(1);
// join the room
await roomsList[0].joinRoom();
// the participant should see the main room as the only breakout room
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name === MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not join breakout room'
});
// the second participant should see one participant in the breakout room
await p2.driver.waitUntil(
async () => {
const list = await p2.getBreakoutRooms().getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
});
});
it('leave breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// leave room
await p1BreakoutRooms.leaveBreakoutRoom();
// there should be one breakout room and that should not be the main room
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name !== MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not leave breakout room'
});
// the second participant should see no participants in the breakout room
await p2.driver.waitUntil(
async () => {
const list = await p2.getBreakoutRooms().getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'P2 is seeing p1 in the breakout room'
});
});
it('remove breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// remove the room
await (await p1BreakoutRooms.getRooms())[0].removeRoom();
// there should be no breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
timeout: 2000,
timeoutMsg: 'Breakout room was not removed for p1'
});
// the second participant should also see no breakout rooms
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
timeout: 2000,
timeoutMsg: 'Breakout room was not removed for p2'
});
});
it('auto assign', async () => {
await ensureThreeParticipants(ctx);
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// create two rooms
await p1BreakoutRooms.addBreakoutRoom();
await p1BreakoutRooms.addBreakoutRoom();
// there should be two breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
timeout: 2000,
timeoutMsg: 'Breakout room was not created by p1'
});
// auto assign participants to rooms
await p1BreakoutRooms.autoAssignToBreakoutRooms();
// each room should have one participant
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
});
// the second participant should see one participant in the main room
const p2BreakoutRooms = p2.getBreakoutRooms();
await p2.driver.waitUntil(
async () => {
if (await p2BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p2BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
}, {
timeout: 2000,
timeoutMsg: 'P2 is not seeing p1 in the main room'
});
});
it('close breakout room', async () => {
const { p1, p2, p3 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be two non-empty breakout rooms
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing two breakout rooms'
});
// close the first room
await (await p1BreakoutRooms.getRooms())[0].closeRoom();
// there should be two rooms and first one should be empty
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 0 || list[1].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing an empty breakout room'
});
// there should be two participants in the main room, either p2 or p3 got moved to the main room
const checkParticipants = async (p: Participant) => {
await p.driver.waitUntil(
async () => {
const isInBreakoutRoom = await p.isInBreakoutRoom();
const breakoutRooms = p.getBreakoutRooms();
if (isInBreakoutRoom) {
if (await breakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await breakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list.every(r => { // eslint-disable-line arrow-body-style
return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
});
}
if (await breakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await breakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount + list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
};
await checkParticipants(p2);
await checkParticipants(p3);
});
it('send participants to breakout room', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
// because the participants rejoin so fast, the meeting is not properly ended,
// so the previous breakout rooms would still be there.
// To avoid this issue we use a different meeting
ctx.roomName += '-breakout-rooms';
await ensureTwoParticipants(ctx);
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be no breakout rooms
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
// add one breakout room
await p1BreakoutRooms.addBreakoutRoom();
// there should be one empty room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
timeout: 2000,
timeoutMsg: 'No breakout room added for p1'
});
// send the second participant to the first breakout room
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
// there should be one room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
});
it('collapse breakout room', async () => {
const { p1 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be one breakout room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
// get id of the breakout room participant
const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
async el => {
const id = await el.getAttribute('id');
return id !== '' && id !== null;
}) as ChainablePromiseElement;
const pId = await breakoutRoomItem.getAttribute('id');
const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
expect(await breakoutParticipant.isDisplayed()).toBe(true);
// collapse the first
await (await p1BreakoutRooms.getRooms())[0].collapse();
// the participant should not be visible
expect(await breakoutParticipant.isDisplayed()).toBe(false);
// the collapsed room should still have one participant
expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
});
it('rename breakout room', async () => {
const myNewRoomName = `breakout-${crypto.randomUUID()}`;
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// let's rename breakout room and see it in local and remote
await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name === myNewRoomName;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
await checkSubject(p2, myNewRoomName);
// leave room
await p2.getBreakoutRooms().leaveBreakoutRoom();
// there should be one empty room
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
expect((await p2.getBreakoutRooms().getRooms())[0].name).toBe(myNewRoomName);
// send the second participant to the first breakout room
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
// there should be one room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not rename for p1'
});
await checkSubject(p2, myNewRoomName);
});
});

View File

@@ -0,0 +1,121 @@
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('Codec selection - ', () => {
it('asymmetric codecs', async () => {
await ensureOneParticipant(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
}
}
});
await ensureTwoParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP8', 'VP9', 'AV1' ]
}
}
});
const { p1, p2 } = ctx;
// Check if media is playing on both endpoints.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences.
// Except on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
});
it('asymmetric codecs with AV1', async () => {
await ensureThreeParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
}
}
});
const { p1, p2, p3 } = ctx;
// Check if media is playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
// Except on Firefox because it doesn't support AV1/VP9 encode and AV1 decode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
// If there is a Firefox ep in the call, all other eps will switch to VP9.
if (p1.driver.isFirefox) {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
} else {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
}
});
it('codec switch over', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureTwoParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
}
}
});
const { p1, p2 } = ctx;
// Disable this test on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
return;
}
// Check if p1 and p2 are encoding in VP9 which is the default codec.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
await ensureThreeParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP8' ]
}
}
});
const { p3 } = ctx;
// Check if all three participants are encoding in VP8 now.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
await p3.hangup();
// Check of p1 and p2 have switched to VP9.
await p1.driver.waitUntil(
async () => await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
await p2.driver.waitUntil(
async () => await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
});
});

View File

@@ -0,0 +1,34 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Chat Panel - ', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
});
it('start closed', async () => {
expect(await ctx.p1.getChatPanel().isOpen()).toBe(false);
});
it('open', async () => {
const { p1 } = ctx;
await p1.getToolbar().clickChatButton();
expect(await p1.getChatPanel().isOpen()).toBe(true);
});
it('use shortcut to close', async () => {
const chatPanel = ctx.p1.getChatPanel();
await chatPanel.pressShortcut();
expect(await chatPanel.isOpen()).toBe(false);
});
it('use shortcut to open', async () => {
const chatPanel = ctx.p1.getChatPanel();
await chatPanel.pressShortcut();
expect(await chatPanel.isOpen()).toBe(true);
});
it('use button to open', async () => {
const { p1 } = ctx;
await p1.getToolbar().clickCloseChatButton();
expect(await p1.getChatPanel().isOpen()).toBe(false);
});
});

View File

@@ -1,6 +1,7 @@
import AllureReporter from '@wdio/allure-reporter';
import { multiremotebrowser } from '@wdio/globals';
import { Buffer } from 'buffer';
import minimatch from 'minimatch';
import path from 'node:path';
import process from 'node:process';
import pretty from 'pretty';
@@ -46,7 +47,7 @@ if (process.env.HEADLESS === 'true') {
chromeArgs.push('--window-size=1280,720');
}
if (process.env.VIDEO_CAPTURE_FILE) {
chromeArgs.push(`use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
chromeArgs.push(`--use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
}
const chromePreferences = {
@@ -62,7 +63,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
specs: [
'specs/**'
],
maxInstances: 1,
maxInstances: 1, // if changing check onWorkerStart logic
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/',
tsConfigPath: './tsconfig.json',
@@ -178,9 +179,6 @@ export const config: WebdriverIO.MultiremoteConfig = {
return;
}
// if (process.env.GRID_HOST_URL) {
// TODO: make sure we use uploadFile only with chrome (it does not work with FF),
// we need to test it with the grid and FF, does it work there
const rpath = await bInstance.uploadFile('tests/resources/iframeAPITest.html');
// @ts-ignore
@@ -199,7 +197,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
after() {
const { ctx }: any = global;
if (ctx.webhooksProxy) {
if (ctx?.webhooksProxy) {
ctx.webhooksProxy.disconnect();
}
},
@@ -239,13 +237,36 @@ export const config: WebdriverIO.MultiremoteConfig = {
* @param {Object} context - The context object.
*/
beforeTest(test, context) {
ctx.skipSuiteTests && context.skip();
if (ctx.skipSuiteTests) {
context.skip();
return;
}
multiremotebrowser.instances.forEach((instance: string) => {
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.title} ===---`);
});
},
/**
* Gets executed before a worker process is spawned and can be used to initialize specific service
* for that worker as well as modify runtime environments in an async fashion.
*/
onWorkerStart(...args) {
// We run a worker per suite, and replay on this logic here
if (args[2].length > 1) {
console.warn('Our worker is supposed to get a single suite, but got more than one');
return;
}
// We skip the suite tests if the suite is marked as such, we used that from firefox overwrite
// @ts-ignore
if (config?.ffExcludes.some((e: string) => minimatch(args[2][0].replace('file://', ''), `${__dirname}/${e}`))) {
args[2].pop();
}
},
/**
* Function to be executed after a test (in Mocha/Jasmine only).
*

View File

@@ -1,11 +1,11 @@
// wdio.dev.conf.ts
// extends te main configuration file for the development environment (make dev)
// extends the main configuration file for the development environment (make dev)
// it will connect to the webpack-dev-server running locally on port 8080
import { deepmerge } from 'deepmerge-ts';
import { merge } from 'lodash-es';
// @ts-ignore
import { config as defaultConfig } from './wdio.conf.ts';
export const config = deepmerge(defaultConfig, {
export const config = merge(defaultConfig, {
baseUrl: 'https://127.0.0.1:8080/torture'
}, { clone: false });

View File

@@ -1,5 +1,5 @@
// wdio.firefox.conf.ts
// extends te main configuration file changing first participant to be Firefox
// extends the main configuration file changing first participant to be Firefox
import { merge } from 'lodash-es';
import process from 'node:process';
@@ -19,11 +19,13 @@ if (process.env.HEADLESS === 'true') {
ffArgs.push('--headless');
}
export const config = merge(defaultConfig, {
exclude: [
'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
],
const ffExcludes = [
'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
];
const mergedConfig = merge(defaultConfig, {
ffExcludes,
capabilities: {
participant1: {
capabilities: {
@@ -34,6 +36,36 @@ export const config = merge(defaultConfig, {
},
acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true'
}
},
participant2: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant2.capabilities['wdio:exclude'],
...ffExcludes
]
}
},
participant3: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant3.capabilities['wdio:exclude'],
...ffExcludes
]
}
},
participant4: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant4.capabilities['wdio:exclude'],
...ffExcludes
]
}
}
}
}, { clone: false });
// Remove the chrome options from the first participant
// @ts-ignore
mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'] = undefined;
export const config = mergedConfig;

View File

@@ -1,6 +1,6 @@
// wdio.grid.conf.ts
// extends the main configuration file to add the selenium grid address
import { deepmerge } from 'deepmerge-ts';
import { merge } from 'lodash-es';
import { URL } from 'url';
// @ts-ignore
@@ -9,7 +9,7 @@ import { config as defaultConfig } from './wdio.conf.ts';
const gridUrl = new URL(process.env.GRID_HOST_URL as string);
const protocol = gridUrl.protocol.replace(':', '');
export const config = deepmerge(defaultConfig, {
export const config = merge(defaultConfig, {
protocol,
hostname: gridUrl.hostname,
port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number

View File

@@ -114,7 +114,7 @@ android {
// Trusted Web Activity.
resValue "color", "backgroundColor", twaManifest.backgroundColor
// Defines a provider authority fot the Splash Screen
// Defines a provider authority for the Splash Screen
resValue "string", "providerAuthority", twaManifest.applicationId + '.fileprovider'
// The enableNotification resource is used to enable or disable the