Compare commits

...

11 Commits

Author SHA1 Message Date
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
33 changed files with 424 additions and 151 deletions

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

@@ -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 d’une source stéréo n’est 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 l’enregistrement 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';

14
package-lock.json generated
View File

@@ -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",
@@ -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",

View File

@@ -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

@@ -185,13 +185,20 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
inIframe?: boolean;
isPromotedFromVisitor?: boolean;
isVisitor?: boolean;
overwritesCustomButtonsWithURL?: 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 +242,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 +282,29 @@ 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; }) => {
if (typeof icon !== 'string') { // The icon will be ignored
return false;
}
try {
const url = new URL(icon);
if (url.protocol === 'data:') {
return false;
}
} catch {
return false;
}
return true;
}));
// Optionally, include local deployment information based on the
// contents of window.config.deploymentInfo.
if (deploymentInfo) {

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

@@ -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

@@ -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

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

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

@@ -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

@@ -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

@@ -5,6 +5,7 @@ import { multiremotebrowser } from '@wdio/globals';
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';
@@ -121,7 +122,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
}
@@ -338,6 +342,13 @@ export class Participant {
});
}
/**
* Returns the chat panel for this participant.
*/
getChatPanel(): ChatPanel {
return new ChatPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*

View File

@@ -28,26 +28,25 @@ export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions
* 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;
}, options),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, options)
]);
await Promise.all([
p2.waitForRemoteStreams(2),
p3.waitForRemoteStreams(2)
ctx.p2.waitForRemoteStreams(2),
ctx.p3.waitForRemoteStreams(2)
]);
}

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,11 @@ export type IContext = {
export type IJoinOptions = {
/**
* Config overwrites to use.
*/
configOverwrite?: IConfig;
/**
* Whether to skip setting display name.
*/

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

@@ -2,6 +2,8 @@ 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 OVERFLOW_MENU = 'More actions menu';
const OVERFLOW = 'More actions';
@@ -142,6 +144,22 @@ export default class Toolbar extends BasePageObject {
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();
}
/**
* Ensure the overflow menu is open and clicks on a specified button.
* @param accessibilityLabel The accessibility label of the button to be clicked.

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';
@@ -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',
@@ -236,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

@@ -24,7 +24,8 @@ const ffExcludes = [
'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
];
export const config = merge(defaultConfig, {
const mergedConfig = merge(defaultConfig, {
ffExcludes,
capabilities: {
participant1: {
capabilities: {
@@ -62,3 +63,9 @@ export const config = merge(defaultConfig, {
}
}
}, { clone: false });
// Remove the chrome options from the first participant
// @ts-ignore
mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'] = undefined;
export const config = mergedConfig;