{
if (this.state.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE
&& this.state.shouldRecordTranscription) {
- dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
+ dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage, true));
} else {
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: this.state.shouldRecordTranscription
diff --git a/react/features/recording/components/Recording/AbstractStopRecordingDialog.ts b/react/features/recording/components/Recording/AbstractStopRecordingDialog.ts
index 3b9fe832c31a..a49a69e0d9bc 100644
--- a/react/features/recording/components/Recording/AbstractStopRecordingDialog.ts
+++ b/react/features/recording/components/Recording/AbstractStopRecordingDialog.ts
@@ -108,7 +108,8 @@ export default class AbstractStopRecordingDialog
}
// TODO: this should be an action in transcribing. -saghul
- this.props.dispatch(setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage));
+ this.props.dispatch(
+ setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage, true));
this.props._conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false
diff --git a/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx b/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx
index 9d837cb0ad2b..10ceaf115560 100644
--- a/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx
+++ b/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx
@@ -8,20 +8,21 @@ import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDial
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants';
import Link from '../../../../base/react/components/native/Link';
+import { IRecordingConsentDialogProps } from '../../../reducer';
import styles from '../styles.native';
/**
* Component that renders the dialog for explicit consent for recordings.
*
+ * @param {IRecordingConsentDialogProps} props - The component props.
* @returns {JSX.Element}
*/
-export default function RecordingConsentDialog() {
+export default function RecordingConsentDialog({ audioWasMuted = false, videoWasMuted = false }: IRecordingConsentDialogProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
const { consentLearnMoreLink } = recordings ?? {};
-
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
@@ -32,11 +33,13 @@ export default function RecordingConsentDialog() {
const consentAndUnmute = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
- dispatch(setAudioMuted(false, true));
- dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
+
+ // Restore to the mute state before consent was requested.
+ dispatch(setAudioMuted(audioWasMuted, false));
+ dispatch(setVideoMuted(videoWasMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
return true;
- }, []);
+ }, [ audioWasMuted, videoWasMuted ]);
return (
{
position: 'relative' as const
},
disabled: {
- background: theme.palette.text02
+ background: theme.palette.recordingHighlightButtonDisabled
},
regular: {
- background: theme.palette.ui10
+ background: theme.palette.recordingHighlightButton
},
highlightNotification: {
- backgroundColor: theme.palette.ui10,
+ backgroundColor: theme.palette.recordingHighlightButton,
borderRadius: '6px',
boxShadow: '0px 6px 20px rgba(0, 0, 0, 0.25)',
boxSizing: 'border-box' as const,
- color: theme.palette.uiBackground,
+ color: theme.palette.recordingNotificationText,
fontSize: '0.875rem',
fontWeight: 400,
left: '4px',
@@ -80,7 +80,7 @@ const styles = (theme: Theme) => {
width: 320
},
highlightNotificationButton: {
- color: theme.palette.action01,
+ color: theme.palette.recordingNotificationAction,
cursor: 'pointer',
fontWeight: 600,
marginTop: '8px'
@@ -203,7 +203,7 @@ export class HighlightButton extends AbstractHighlightButton {
diff --git a/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx b/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx
index 883aa402e1ef..cba13dea5160 100644
--- a/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx
+++ b/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx
@@ -10,6 +10,7 @@ import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../../
/**
* Component that renders the dialog for explicit consent for recordings.
+ * The prejoin mute state is read from Redux by the action creator.
*
* @returns {JSX.Element}
*/
diff --git a/react/features/recording/functions.ts b/react/features/recording/functions.ts
index c5fedfec536b..05ed2fa61df3 100644
--- a/react/features/recording/functions.ts
+++ b/react/features/recording/functions.ts
@@ -248,7 +248,7 @@ export function getRecordButtonProps(state: IReduxState) {
// a button can be disabled/enabled if enableFeaturesBasedOnToken
// is on or if the livestreaming is running.
let disabled = false;
- let tooltip = '';
+ let tooltip = isRecordingRunning(state) ? 'dialog.stopRecording' : 'dialog.startRecording';
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomus and decide on
diff --git a/react/features/recording/middleware.ts b/react/features/recording/middleware.ts
index 59e71989eb39..41dd70d7629d 100644
--- a/react/features/recording/middleware.ts
+++ b/react/features/recording/middleware.ts
@@ -421,12 +421,21 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
return;
}
+ // Capture the current mute state BEFORE forcing mute for consent
+ // This preserves the user's intentional mute choices from prejoin or initial settings
+ const state = getState();
+ const audioWasMuted = state['features/base/media'].audio.muted;
+ const videoWasMuted = state['features/base/media'].video.muted;
+
batch(() => {
dispatch(markConsentRequested(recorderSession.getID()));
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
- dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog));
+ dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog, {
+ audioWasMuted,
+ videoWasMuted
+ }));
});
}
diff --git a/react/features/recording/reducer.ts b/react/features/recording/reducer.ts
index 17f3489be5fa..0e8244fd0bb2 100644
--- a/react/features/recording/reducer.ts
+++ b/react/features/recording/reducer.ts
@@ -42,6 +42,14 @@ export interface IRecordingState {
wasStartRecordingSuggested?: boolean;
}
+/**
+ * Props for the RecordingConsentDialog component.
+ */
+export interface IRecordingConsentDialogProps {
+ audioWasMuted?: boolean;
+ videoWasMuted?: boolean;
+}
+
/**
* The name of the Redux store this feature stores its state in.
*/
diff --git a/react/features/salesforce/components/web/SalesforceLinkDialog.tsx b/react/features/salesforce/components/web/SalesforceLinkDialog.tsx
index f8b38a7ef9ce..bbe33c413edd 100644
--- a/react/features/salesforce/components/web/SalesforceLinkDialog.tsx
+++ b/react/features/salesforce/components/web/SalesforceLinkDialog.tsx
@@ -28,7 +28,7 @@ const useStyles = makeStyles()(theme => {
searchIcon: {
display: 'block',
position: 'absolute',
- color: theme.palette.text03,
+ color: theme.palette.salesforceSearchIcon,
left: 16,
top: 10,
width: 20,
@@ -39,16 +39,16 @@ const useStyles = makeStyles()(theme => {
margin: '16px 0 8px'
},
recordsSearch: {
- backgroundColor: theme.palette.field01,
+ backgroundColor: theme.palette.salesforceSearchBackground,
border: '1px solid',
borderRadius: theme.shape.borderRadius,
- borderColor: theme.palette.ui05,
- color: theme.palette.text01,
+ borderColor: theme.palette.salesforceSearchBorder,
+ color: theme.palette.salesforceSearchText,
padding: '10px 16px 10px 44px',
width: '100%',
height: 40,
'&::placeholder': {
- color: theme.palette.text03,
+ color: theme.palette.salesforceSearchPlaceholder,
...theme.typography.bodyShortRegular
}
},
@@ -94,7 +94,7 @@ const useStyles = makeStyles()(theme => {
padding: 0
},
recordInfo: {
- backgroundColor: theme.palette.ui03,
+ backgroundColor: theme.palette.inputFieldBackground,
padding: '0 16px',
borderRadius: theme.shape.borderRadius,
marginBottom: '28px'
@@ -113,9 +113,9 @@ const useStyles = makeStyles()(theme => {
boxSizing: 'border-box',
overflow: 'hidden',
border: '1px solid',
- borderColor: theme.palette.ui05,
- backgroundColor: theme.palette.field01,
- color: theme.palette.text01,
+ borderColor: theme.palette.salesforceSearchBorder,
+ backgroundColor: theme.palette.inputBackground,
+ color: theme.palette.inputText,
borderRadius: theme.shape.borderRadius,
padding: '10px 16px'
}
diff --git a/react/features/settings/components/native/LanguageSelectView.tsx b/react/features/settings/components/native/LanguageSelectView.tsx
index 7e471a777615..ad4ef74dfa89 100644
--- a/react/features/settings/components/native/LanguageSelectView.tsx
+++ b/react/features/settings/components/native/LanguageSelectView.tsx
@@ -1,7 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
+import { GestureResponderEvent, ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import { Edge } from 'react-native-safe-area-context';
import { useSelector } from 'react-redux';
@@ -11,12 +11,13 @@ import { IconArrowLeft } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
-import { goBack, navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
-import { screen } from '../../../mobile/navigation/routes';
import styles from './styles';
-const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
+const LanguageSelectView = ({ goBack, isInWelcomePage }: {
+ goBack?: (e?: GestureResponderEvent | React.MouseEvent) => void;
+ isInWelcomePage?: boolean;
+}) => {
const { t } = useTranslation();
const navigation = useNavigation();
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
@@ -25,7 +26,7 @@ const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; })
const setLanguage = useCallback(language => () => {
i18next.changeLanguage(language);
conference?.setTranscriptionLanguage(language);
- navigate(screen.settings.main);
+ goBack?.();
}, [ conference, i18next ]);
const headerLeft = () => (
diff --git a/react/features/settings/components/web/CalendarTab.tsx b/react/features/settings/components/web/CalendarTab.tsx
index 544e4b301804..65e2c2740e46 100644
--- a/react/features/settings/components/web/CalendarTab.tsx
+++ b/react/features/settings/components/web/CalendarTab.tsx
@@ -77,7 +77,7 @@ const styles = (theme: Theme) => {
justifyContent: 'center',
textAlign: 'center' as const,
minHeight: '100px',
- color: theme.palette.text01,
+ color: theme.palette.settingsTabText,
...theme.typography.bodyShortRegular
},
diff --git a/react/features/settings/components/web/ModeratorTab.tsx b/react/features/settings/components/web/ModeratorTab.tsx
index cdb52474e8ad..8a10edb4c206 100644
--- a/react/features/settings/components/web/ModeratorTab.tsx
+++ b/react/features/settings/components/web/ModeratorTab.tsx
@@ -90,7 +90,7 @@ const styles = (theme: Theme) => {
title: {
...theme.typography.heading6,
- color: `${theme.palette.text01} !important`,
+ color: `${theme.palette.settingsTabText} !important`,
marginBottom: theme.spacing(3)
},
diff --git a/react/features/settings/components/web/MoreTab.tsx b/react/features/settings/components/web/MoreTab.tsx
index 4dfad6481930..4d94be7cbd45 100644
--- a/react/features/settings/components/web/MoreTab.tsx
+++ b/react/features/settings/components/web/MoreTab.tsx
@@ -97,7 +97,7 @@ const styles = (theme: Theme) => {
width: '100%',
height: '1px',
border: 0,
- backgroundColor: theme.palette.ui03
+ backgroundColor: theme.palette.settingsSectionBackground
},
checkbox: {
diff --git a/react/features/settings/components/web/NotificationsTab.tsx b/react/features/settings/components/web/NotificationsTab.tsx
index 317451a87c47..f7fb9de147f1 100644
--- a/react/features/settings/components/web/NotificationsTab.tsx
+++ b/react/features/settings/components/web/NotificationsTab.tsx
@@ -107,7 +107,7 @@ const styles = (theme: Theme) => {
title: {
...theme.typography.heading6,
- color: `${theme.palette.text01} !important`,
+ color: `${theme.palette.settingsTabText} !important`,
marginBottom: theme.spacing(3)
},
diff --git a/react/features/settings/components/web/ProfileTab.tsx b/react/features/settings/components/web/ProfileTab.tsx
index 2428aef3eab8..55a500096ad9 100644
--- a/react/features/settings/components/web/ProfileTab.tsx
+++ b/react/features/settings/components/web/ProfileTab.tsx
@@ -87,7 +87,7 @@ const styles = (theme: Theme) => {
},
label: {
- color: `${theme.palette.text01} !important`,
+ color: `${theme.palette.settingsTabText} !important`,
...theme.typography.bodyShortRegular,
marginBottom: theme.spacing(2)
},
diff --git a/react/features/settings/components/web/ShortcutsTab.tsx b/react/features/settings/components/web/ShortcutsTab.tsx
index 379924b280ba..dc50bef189d8 100644
--- a/react/features/settings/components/web/ShortcutsTab.tsx
+++ b/react/features/settings/components/web/ShortcutsTab.tsx
@@ -59,11 +59,11 @@ const styles = (theme: Theme) => {
alignItems: 'center',
padding: `${theme.spacing(1)} 0`,
...theme.typography.bodyShortRegular,
- color: theme.palette.text01
+ color: theme.palette.settingsTabText
},
listItemKey: {
- backgroundColor: theme.palette.ui04,
+ backgroundColor: theme.palette.settingsShortcutKey,
...theme.typography.labelBold,
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
diff --git a/react/features/settings/components/web/audio/MicrophoneEntry.tsx b/react/features/settings/components/web/audio/MicrophoneEntry.tsx
index 7a807b276ebb..8f11c70284a7 100644
--- a/react/features/settings/components/web/audio/MicrophoneEntry.tsx
+++ b/react/features/settings/components/web/audio/MicrophoneEntry.tsx
@@ -92,7 +92,7 @@ const useStyles = makeStyles()(theme => {
marginLeft: '6px',
'& svg': {
- fill: theme.palette.iconError
+ fill: theme.palette.settingsErrorIcon
}
},
diff --git a/react/features/settings/components/web/video/VideoSettingsContent.tsx b/react/features/settings/components/web/video/VideoSettingsContent.tsx
index 2c2c9999dfb1..133610f637fa 100644
--- a/react/features/settings/components/web/video/VideoSettingsContent.tsx
+++ b/react/features/settings/components/web/video/VideoSettingsContent.tsx
@@ -95,6 +95,10 @@ const useStyles = makeStyles()((theme) => {
},
},
+ selectedEntry: {
+ border: `2px solid ${theme.palette.settingsVideoPreviewBorder}`
+ },
+
previewVideo: {
borderRadius: "12px",
overflow: "hidden",
@@ -123,7 +127,7 @@ const useStyles = makeStyles()((theme) => {
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: "16px",
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
- color: theme.palette.text01,
+ color: theme.palette.settingsTabText,
// ...withPixelLineHeight(theme.typography.labelBold),
// width: "fit-content",
...theme.typography.labelBold,
diff --git a/react/features/speaker-stats/components/web/SpeakerStats.tsx b/react/features/speaker-stats/components/web/SpeakerStats.tsx
index 0cafe3d2759b..4cfe6c991190 100644
--- a/react/features/speaker-stats/components/web/SpeakerStats.tsx
+++ b/react/features/speaker-stats/components/web/SpeakerStats.tsx
@@ -33,7 +33,7 @@ const useStyles = makeStyles()(theme => {
speakerStats: {
'& .header': {
position: 'fixed',
- backgroundColor: theme.palette.ui01,
+ backgroundColor: theme.palette.speakerStatsBackground,
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4),
marginLeft: `-${theme.spacing(4)}`,
@@ -95,7 +95,7 @@ const useStyles = makeStyles()(theme => {
display: 'flex',
alignItems: 'center',
borderLeftWidth: 1,
- borderLeftColor: theme.palette.ui02,
+ borderLeftColor: theme.palette.speakerStatsBorder,
borderLeftStyle: 'solid',
'& .timeline': {
height: theme.spacing(2),
@@ -127,7 +127,7 @@ const useStyles = makeStyles()(theme => {
height: theme.spacing(1),
display: 'flex',
width: '100%',
- backgroundColor: theme.palette.ui03,
+ backgroundColor: theme.palette.speakerStatsRowAlternate,
position: 'relative',
'& .left-bound': {
position: 'absolute',
@@ -141,7 +141,7 @@ const useStyles = makeStyles()(theme => {
},
'& .handler': {
position: 'absolute',
- backgroundColor: theme.palette.ui09,
+ backgroundColor: theme.palette.speakerStatsHeaderBackground,
height: 12,
marginTop: -4,
display: 'flex',
@@ -159,7 +159,7 @@ const useStyles = makeStyles()(theme => {
width: 'calc(100% + 48px)',
height: 1,
marginLeft: -24,
- backgroundColor: theme.palette.ui02
+ backgroundColor: theme.palette.speakerStatsBorder
}
}
};
diff --git a/react/features/speaker-stats/components/web/SpeakerStatsItem.tsx b/react/features/speaker-stats/components/web/SpeakerStatsItem.tsx
index d2a9977acd84..d3969ea84b3c 100644
--- a/react/features/speaker-stats/components/web/SpeakerStatsItem.tsx
+++ b/react/features/speaker-stats/components/web/SpeakerStatsItem.tsx
@@ -78,7 +78,7 @@ const SpeakerStatsItem = (props: IProps) => {
props.hasLeft ? (
diff --git a/react/features/speaker-stats/components/web/SpeakerStatsList.tsx b/react/features/speaker-stats/components/web/SpeakerStatsList.tsx
index b7e162bfee9e..829abb06eb75 100644
--- a/react/features/speaker-stats/components/web/SpeakerStatsList.tsx
+++ b/react/features/speaker-stats/components/web/SpeakerStatsList.tsx
@@ -16,7 +16,7 @@ const useStyles = makeStyles()(theme => {
height: theme.spacing(8)
},
'& .has-left': {
- color: theme.palette.text03
+ color: theme.palette.speakerStatsLabelText
},
'& .avatar': {
marginRight: theme.spacing(3)
@@ -28,7 +28,7 @@ const useStyles = makeStyles()(theme => {
[theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
...theme.typography.bodyShortRegularLarge
},
- backgroundColor: theme.palette.ui02
+ backgroundColor: theme.palette.speakerStatsRowBackground
},
'& .display-name': {
...theme.typography.bodyShortRegular,
@@ -37,7 +37,7 @@ const useStyles = makeStyles()(theme => {
}
},
'& .dominant': {
- backgroundColor: theme.palette.success02
+ backgroundColor: theme.palette.speakerStatsSuccessBar
}
}
diff --git a/react/features/speaker-stats/components/web/SpeakerStatsSearch.tsx b/react/features/speaker-stats/components/web/SpeakerStatsSearch.tsx
index a807a87f0413..9d991695e56b 100644
--- a/react/features/speaker-stats/components/web/SpeakerStatsSearch.tsx
+++ b/react/features/speaker-stats/components/web/SpeakerStatsSearch.tsx
@@ -20,7 +20,7 @@ const useStyles = makeStyles()(theme => {
[theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
display: 'block',
position: 'absolute',
- color: theme.palette.text03,
+ color: theme.palette.speakerStatsSearchPlaceholder,
left: 16,
top: 13,
width: 20,
@@ -28,16 +28,16 @@ const useStyles = makeStyles()(theme => {
}
},
speakerStatsSearch: {
- backgroundColor: theme.palette.field01,
+ backgroundColor: theme.palette.speakerStatsSearchBackground,
border: '1px solid',
borderRadius: 6,
- borderColor: theme.palette.ui05,
- color: theme.palette.text01,
+ borderColor: theme.palette.speakerStatsSearchBorder,
+ color: theme.palette.speakerStatsSearchText,
padding: '10px 16px',
width: '100%',
height: 40,
'&::placeholder': {
- color: theme.palette.text03,
+ color: theme.palette.speakerStatsSearchPlaceholder,
...theme.typography.bodyShortRegular
},
[theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
@@ -103,7 +103,7 @@ function SpeakerStatsSearch({ onSearch }: IProps) {
{
return {
itemContainer: {
display: 'flex',
- color: theme.palette.text02,
+ color: theme.palette.dialogSecondaryText,
alignItems: 'center',
fontSize: '0.875rem',
cursor: 'pointer',
padding: '5px 0',
'&:hover': {
- backgroundColor: theme.palette.ui04
+ backgroundColor: theme.palette.languageSelectorHover
}
},
iconWrapper: {
diff --git a/react/features/subtitles/components/web/LanguageSelector.tsx b/react/features/subtitles/components/web/LanguageSelector.tsx
index 04ceb3568553..9ea9d1bc58bb 100644
--- a/react/features/subtitles/components/web/LanguageSelector.tsx
+++ b/react/features/subtitles/components/web/LanguageSelector.tsx
@@ -28,7 +28,7 @@ const useStyles = makeStyles()(theme => {
},
label: {
...theme.typography.bodyShortRegular,
- color: theme.palette.text01,
+ color: theme.palette.languageSelectorText,
whiteSpace: 'nowrap'
}
};
@@ -39,7 +39,6 @@ const useStyles = makeStyles()(theme => {
* Uses the same language options as LanguageSelectorDialog and
* updates the subtitles language preference in Redux.
*
- * @param {IProps} props - The component props.
* @returns {JSX.Element} - The rendered component.
*/
function LanguageSelector() {
@@ -51,6 +50,13 @@ function LanguageSelector() {
state,
selectedLanguage?.replace('translation-languages:', '')
));
+ const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
+ state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
+
+ // Hide the "Translate to" option when asyncTranscription is enabled
+ if (isAsyncTranscriptionEnabled) {
+ return null;
+ }
/**
* Maps available languages to Select component options format.
diff --git a/react/features/subtitles/components/web/LanguageSelectorDialog.tsx b/react/features/subtitles/components/web/LanguageSelectorDialog.tsx
index 84dbf8575122..62be88dd4e06 100644
--- a/react/features/subtitles/components/web/LanguageSelectorDialog.tsx
+++ b/react/features/subtitles/components/web/LanguageSelectorDialog.tsx
@@ -19,14 +19,14 @@ const useStyles = makeStyles()(theme => {
paragraphWrapper: {
fontSize: '0.875rem',
margin: '10px 0px',
- color: theme.palette.text01
+ color: theme.palette.dialogText
},
spanWrapper: {
fontWeight: 700,
cursor: 'pointer',
color: theme.palette.link01,
'&:hover': {
- backgroundColor: theme.palette.ui04,
+ backgroundColor: theme.palette.languageSelectorHover,
color: theme.palette.link01Hover
}
}
diff --git a/react/features/subtitles/middleware.ts b/react/features/subtitles/middleware.ts
index 50373bd813c7..8235b88df709 100644
--- a/react/features/subtitles/middleware.ts
+++ b/react/features/subtitles/middleware.ts
@@ -98,7 +98,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case SET_REQUESTING_SUBTITLES:
- _requestingSubtitlesChange(store, action.enabled, action.language);
+ _requestingSubtitlesChange(store, action.enabled, action.language, action.forceBackendRecordingOn);
break;
}
@@ -344,13 +344,16 @@ function _getPrimaryLanguageCode(language: string) {
* @param {Store} store - The redux store.
* @param {boolean} enabled - Whether subtitles should be enabled or not.
* @param {string} language - The language to use for translation.
+ * @param {boolean} forceBackendRecordingOn - Whether to force backend recording is on or not. This is used only when
+ * we start recording, stopping is based on whether isTranscribingEnabled is already set.
* @private
* @returns {void}
*/
function _requestingSubtitlesChange(
{ dispatch, getState }: IStore,
enabled: boolean,
- language?: string | null) {
+ language?: string | null,
+ forceBackendRecordingOn: boolean = false) {
const state = getState();
const { conference } = state['features/base/conference'];
const backendRecordingOn = conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription;
@@ -375,7 +378,9 @@ function _requestingSubtitlesChange(
}));
dispatch(setSubtitlesError(true));
});
- } else {
+ }
+
+ if (backendRecordingOn || forceBackendRecordingOn) {
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
@@ -388,7 +393,7 @@ function _requestingSubtitlesChange(
language.replace('translation-languages:', ''));
}
- if (!enabled && backendRecordingOn
+ if (!enabled && (backendRecordingOn || forceBackendRecordingOn)
&& conference?.getMetadataHandler()?.getMetadata()[RECORDING_METADATA_ID]?.isTranscribingEnabled) {
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false
diff --git a/react/features/toolbox/components/web/Drawer.tsx b/react/features/toolbox/components/web/Drawer.tsx
index a3b4cac47e61..bcd354b19467 100644
--- a/react/features/toolbox/components/web/Drawer.tsx
+++ b/react/features/toolbox/components/web/Drawer.tsx
@@ -44,7 +44,7 @@ const useStyles = makeStyles()(theme => {
},
drawer: {
- backgroundColor: theme.palette.ui01,
+ backgroundColor: theme.palette.drawerBackground,
maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
borderRadius: '24px 24px 0 0',
overflowY: 'auto',
@@ -64,7 +64,7 @@ const useStyles = makeStyles()(theme => {
height: '48px',
padding: '12px 16px',
alignItems: 'center',
- color: theme.palette.text01,
+ color: theme.palette.overflowMenuItemText,
cursor: 'pointer',
display: 'flex',
fontSize: '1rem',
@@ -72,12 +72,34 @@ const useStyles = makeStyles()(theme => {
'& div': {
display: 'flex',
flexDirection: 'row',
- alignItems: 'center'
+ alignItems: 'center',
+
+ '& svg': {
+ fill: theme.palette.overflowMenuItemIcon
+ }
+ },
+
+ '& > svg': {
+ fill: theme.palette.overflowMenuItemIcon
+ },
+
+ '@media (hover: hover) and (pointer: fine)': {
+ '&:hover': {
+ backgroundColor: theme.palette.overflowMenuItemHover,
+
+ '& svg': {
+ fill: theme.palette.overflowMenuItemIcon
+ }
+ }
},
'&.disabled': {
cursor: 'initial',
- color: '#3b475c'
+ color: theme.palette.overflowMenuItemDisabled,
+
+ '& svg': {
+ fill: theme.palette.overflowMenuItemDisabled
+ }
}
}
}
diff --git a/react/features/toolbox/components/web/JitsiPortal.tsx b/react/features/toolbox/components/web/JitsiPortal.tsx
index 5dbf86812405..6caae9b491f6 100644
--- a/react/features/toolbox/components/web/JitsiPortal.tsx
+++ b/react/features/toolbox/components/web/JitsiPortal.tsx
@@ -32,7 +32,7 @@ const useStyles = makeStyles()(theme => {
'&::after': {
content: '""',
- backgroundColor: theme.palette.ui01,
+ backgroundColor: theme.palette.toolboxBackground,
marginBottom: 'env(safe-area-inset-bottom, 0)'
}
}
diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts
index 4ed35f942d8c..5116c6e573b8 100644
--- a/react/features/toolbox/constants.ts
+++ b/react/features/toolbox/constants.ts
@@ -123,7 +123,8 @@ export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'embedmeeting',
'feedback',
'download',
- 'help'
+ 'help',
+ 'custom-panel'
];
export const TOOLBAR_TIMEOUT = 4000;
@@ -148,6 +149,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'camera',
'chat',
'closedcaptions',
+ 'custom-panel',
'desktop',
'download',
'embedmeeting',
diff --git a/react/features/toolbox/hooks.web.ts b/react/features/toolbox/hooks.web.ts
index 82bd978c58e4..72de12b255fc 100644
--- a/react/features/toolbox/hooks.web.ts
+++ b/react/features/toolbox/hooks.web.ts
@@ -15,6 +15,7 @@ import { isToggleCameraEnabled } from '../base/tracks/functions.web';
import { toggleChat } from '../chat/actions.web';
import { isChatDisabled } from '../chat/functions';
import { useChatButton } from '../chat/hooks.web';
+import { useCustomPanelButton } from '../custom-panel/hooks.web';
import { useEmbedButton } from '../embed-meeting/hooks';
import { useEtherpadButton } from '../etherpad/hooks';
import { useFeedbackButton } from '../feedback/hooks.web';
@@ -315,6 +316,7 @@ export function useToolboxButtons(
const feedback = useFeedbackButton();
const _download = useDownloadButton();
const _help = useHelpButton();
+ const customPanel = useCustomPanelButton();
const buttons: { [key in ToolbarButton]?: IToolboxButton; } = {
microphone,
@@ -349,7 +351,8 @@ export function useToolboxButtons(
embedmeeting: embed,
feedback,
download: _download,
- help: _help
+ help: _help,
+ 'custom-panel': customPanel
};
const buttonKeys = Object.keys(buttons) as ToolbarButton[];
diff --git a/react/features/toolbox/types.ts b/react/features/toolbox/types.ts
index b61833110dff..d5f0c84170fc 100644
--- a/react/features/toolbox/types.ts
+++ b/react/features/toolbox/types.ts
@@ -20,6 +20,7 @@ export interface IToolboxNativeButton {
export type ToolbarButton = 'camera' |
'chat' |
'closedcaptions' |
+ 'custom-panel' |
'desktop' |
'download' |
'embedmeeting' |
diff --git a/react/features/video-menu/actions.any.ts b/react/features/video-menu/actions.any.ts
index a324624b8813..a48ab22f8130 100644
--- a/react/features/video-menu/actions.any.ts
+++ b/react/features/video-menu/actions.any.ts
@@ -73,11 +73,6 @@ export function muteRemote(participantId: string, mediaType: MediaType) {
const muteMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
dispatch(muteRemoteParticipant(participantId, muteMediaType));
-
- // Notify external API that participant was muted by moderator
- if (typeof APP !== 'undefined') {
- APP.API.notifyParticipantMuted(participantId, true, muteMediaType, false);
- }
};
}
diff --git a/react/features/video-menu/components/web/ParticipantContextMenu.tsx b/react/features/video-menu/components/web/ParticipantContextMenu.tsx
index c9860e804acf..521b9fe5995b 100644
--- a/react/features/video-menu/components/web/ParticipantContextMenu.tsx
+++ b/react/features/video-menu/components/web/ParticipantContextMenu.tsx
@@ -107,7 +107,7 @@ interface IProps {
const useStyles = makeStyles()(theme => {
return {
text: {
- color: theme.palette.text02,
+ color: theme.palette.videoMenuText,
padding: '10px 16px',
height: '40px',
overflow: 'hidden',
diff --git a/react/features/video-menu/components/web/VolumeSlider.tsx b/react/features/video-menu/components/web/VolumeSlider.tsx
index 0dc0e8c8ecff..265b53158303 100644
--- a/react/features/video-menu/components/web/VolumeSlider.tsx
+++ b/react/features/video-menu/components/web/VolumeSlider.tsx
@@ -37,7 +37,7 @@ const useStyles = makeStyles()(theme => {
padding: '10px 16px',
'&:hover': {
- backgroundColor: theme.palette.ui02
+ backgroundColor: theme.palette.videoMenuSliderBackground
}
},
diff --git a/react/features/video-quality/components/Slider.web.tsx b/react/features/video-quality/components/Slider.web.tsx
index 446e0a88ccbb..064bc72a46f3 100644
--- a/react/features/video-quality/components/Slider.web.tsx
+++ b/react/features/video-quality/components/Slider.web.tsx
@@ -44,7 +44,7 @@ const useStyles = makeStyles()(theme => {
height
};
const inputThumb = {
- background: theme.palette.text01,
+ background: theme.palette.sliderKnob,
border: 0,
borderRadius: '50%',
height: 24,
@@ -52,7 +52,7 @@ const useStyles = makeStyles()(theme => {
};
const focused = {
- outline: `1px solid ${theme.palette.ui06}`
+ outline: `1px solid ${theme.palette.sliderFocus}`
};
return {
@@ -72,14 +72,14 @@ const useStyles = makeStyles()(theme => {
width: '100%'
},
knob: {
- background: theme.palette.text01,
+ background: theme.palette.sliderKnob,
borderRadius: '50%',
display: 'inline-block',
height,
width: 6
},
track: {
- background: theme.palette.text03,
+ background: theme.palette.sliderTrack,
borderRadius: Number(theme.shape.borderRadius) / 2,
height
},
diff --git a/react/features/video-quality/components/VideoQualitySlider.web.tsx b/react/features/video-quality/components/VideoQualitySlider.web.tsx
index aa2683e33ef1..9545bdf85a65 100644
--- a/react/features/video-quality/components/VideoQualitySlider.web.tsx
+++ b/react/features/video-quality/components/VideoQualitySlider.web.tsx
@@ -88,14 +88,14 @@ interface IProps extends WithTranslation {
const styles = (theme: Theme) => {
return {
dialog: {
- color: theme.palette.text01
+ color: theme.palette.videoQualityText
},
dialogDetails: {
...theme.typography.bodyShortRegularLarge,
marginBottom: 16
},
dialogContents: {
- background: theme.palette.ui01,
+ background: theme.palette.videoQualityBackground,
padding: '16px 16px 48px 16px'
},
sliderDescription: {
diff --git a/react/features/virtual-background/components/VirtualBackgrounds.tsx b/react/features/virtual-background/components/VirtualBackgrounds.tsx
index f8a7c7437c25..125905f0204a 100644
--- a/react/features/virtual-background/components/VirtualBackgrounds.tsx
+++ b/react/features/virtual-background/components/VirtualBackgrounds.tsx
@@ -99,7 +99,7 @@ const useStyles = makeStyles()(theme => {
},
thumbnail: {
- width: "100%", // Ocupa todo el ancho disponible del grid
+ width: "100%",
aspectRatio: "7 / 4",
borderRadius: "4px",
boxSizing: "border-box",
@@ -107,8 +107,9 @@ const useStyles = makeStyles()(theme => {
alignItems: "center",
justifyContent: "center",
textAlign: "center",
- ...withPixelLineHeight(theme.typography.labelBold),
- color: theme.palette.text01,
+ ...theme.typography.labelBold,
+ color: theme.palette.virtualBackgroundText,
+
objectFit: "cover",
[["&:hover", "&:focus"] as any]: {
@@ -130,7 +131,7 @@ const useStyles = makeStyles()(theme => {
},
noneThumbnail: {
- backgroundColor: theme.palette.ui04,
+ backgroundColor: theme.palette.virtualBackgroundBorder
},
slightBlur: {
@@ -157,7 +158,7 @@ const useStyles = makeStyles()(theme => {
position: "absolute",
top: "3px",
right: "3px",
- background: theme.palette.ui03,
+ background: theme.palette.virtualBackgroundBorder,
borderRadius: "3px",
cursor: "pointer",
display: "none",
diff --git a/react/features/visitors/components/web/VisitorsCountLabel.tsx b/react/features/visitors/components/web/VisitorsCountLabel.tsx
index edc6ee585de1..c14ecd8c70d4 100644
--- a/react/features/visitors/components/web/VisitorsCountLabel.tsx
+++ b/react/features/visitors/components/web/VisitorsCountLabel.tsx
@@ -11,8 +11,8 @@ import { getVisitorsCount, getVisitorsShortText } from '../../functions';
const useStyles = makeStyles()(theme => {
return {
label: {
- backgroundColor: theme.palette.warning02,
- color: theme.palette.uiBackground
+ backgroundColor: theme.palette.visitorsCountBadge,
+ color: theme.palette.visitorsCountText
}
};
});
@@ -28,7 +28,7 @@ const VisitorsCountLabel = () => {
) : null;
diff --git a/react/features/visitors/components/web/VisitorsQueue.tsx b/react/features/visitors/components/web/VisitorsQueue.tsx
index 8f430ebc1140..81c179f0cfa3 100644
--- a/react/features/visitors/components/web/VisitorsQueue.tsx
+++ b/react/features/visitors/components/web/VisitorsQueue.tsx
@@ -14,7 +14,7 @@ const useStyles = makeStyles()(theme => {
position: 'absolute',
inset: '0 0 0 0',
display: 'flex',
- backgroundColor: theme.palette.ui01,
+ backgroundColor: theme.palette.visitorsQueueBackground,
zIndex: 252,
'@media (max-width: 720px)': {
@@ -57,7 +57,7 @@ const useStyles = makeStyles()(theme => {
},
roomName: {
...theme.typography.heading5,
- color: theme.palette.text01,
+ color: theme.palette.visitorsQueueText,
marginBottom: theme.spacing(4),
overflow: 'hidden',
textAlign: 'center',
diff --git a/react/features/welcome/constants.tsx b/react/features/welcome/constants.tsx
index a9b0664e7007..0d4482327d9b 100644
--- a/react/features/welcome/constants.tsx
+++ b/react/features/welcome/constants.tsx
@@ -5,8 +5,8 @@ import BaseTheme from '../base/ui/components/BaseTheme';
import TabIcon from './components/TabIcon';
-export const ACTIVE_TAB_COLOR = BaseTheme.palette.icon01;
-export const INACTIVE_TAB_COLOR = BaseTheme.palette.icon03;
+export const ACTIVE_TAB_COLOR = BaseTheme.palette.welcomeTabActive;
+export const INACTIVE_TAB_COLOR = BaseTheme.palette.welcomeTabInactive;
export const tabBarOptions = {
tabBarActiveTintColor: ACTIVE_TAB_COLOR,
@@ -15,7 +15,7 @@ export const tabBarOptions = {
fontSize: 12,
},
tabBarStyle: {
- backgroundColor: BaseTheme.palette.ui01
+ backgroundColor: BaseTheme.palette.welcomeCard
}
};
diff --git a/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua b/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua
index 56dc511092d0..528e3fdd027d 100644
--- a/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua
+++ b/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua
@@ -76,3 +76,13 @@ local function anonymous(self, message)
end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
+
+module:hook("pre-resource-unbind", function (e)
+ local error, session = e.error, e.session;
+
+ prosody.events.fire_event('jitsi-pre-session-unbind', {
+ jid = session.full_jid,
+ session = session,
+ error = error
+ });
+end, 11);
diff --git a/resources/prosody-plugins/mod_auth_token.lua b/resources/prosody-plugins/mod_auth_token.lua
index d18e2dcc6716..b5b09dc12bfd 100644
--- a/resources/prosody-plugins/mod_auth_token.lua
+++ b/resources/prosody-plugins/mod_auth_token.lua
@@ -61,6 +61,16 @@ end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);
+module:hook("pre-resource-unbind", function (e)
+ local error, session = e.error, e.session;
+
+ prosody.events.fire_event('jitsi-pre-session-unbind', {
+ jid = session.full_jid,
+ session = session,
+ error = error
+ });
+end, 11);
+
function provider.test_password(username, password)
return nil, "Password based auth not supported";
end
diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua
index 2f6f875edb71..4f2a3cb2644b 100644
--- a/resources/prosody-plugins/mod_filter_iq_rayo.lua
+++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua
@@ -95,26 +95,27 @@ module:hook("pre-iq/full", function(event)
local room_jid = jid.bare(stanza.attr.to);
local room_real_jid = room_jid_match_rewrite(room_jid);
local room = main_muc_service.get_room_from_jid(room_real_jid);
- local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil;
-
- if not room or not is_sender_in_room then
- module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
- session.send(st.error_reply(stanza, "auth", "forbidden"));
- return true;
- end
-
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
- local is_session_allowed = is_feature_allowed(
+ local error_message = nil;
+
+ if not room or room:get_occupant_jid(stanza.attr.from) == nil then
+ error_message = "not in room";
+ elseif roomName == nil then
+ error_message = OUT_ROOM_NAME_ATTR_NAME.." header missing";
+ elseif roomName ~= room_jid then
+ error_message = OUT_ROOM_NAME_ATTR_NAME.." header mismatch";
+ elseif (token ~= nil and not token_util:verify_room(session, room_real_jid)) then
+ error_message = "no token or token room mismatch";
+ elseif not is_feature_allowed(
feature,
session.jitsi_meet_context_features,
- room:get_affiliation(stanza.attr.from) == 'owner');
-
- if roomName == nil
- or roomName ~= room_jid
- or (token ~= nil and not token_util:verify_room(session, room_real_jid))
- or not is_session_allowed
- then
- module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
+ room:get_affiliation(stanza.attr.from) == 'owner') then
+
+ error_message = "feature not allowed";
+ end
+
+ if error_message then
+ module:log("warn", "Filtering stanza dial, %s, stanza:%s", error_message, tostring(stanza));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
diff --git a/resources/prosody-plugins/mod_muc_auth_ban.lua b/resources/prosody-plugins/mod_muc_auth_ban.lua
index 4dac9d679e44..11ec52bd9b3b 100644
--- a/resources/prosody-plugins/mod_muc_auth_ban.lua
+++ b/resources/prosody-plugins/mod_muc_auth_ban.lua
@@ -11,8 +11,13 @@ local json = require "cjson.safe";
local http = require "net.http";
local inspect = require 'inspect';
+local util = module:require 'util';
+local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
+local is_vpaas = util.is_vpaas;
+
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
+local ban_check_error_count = module:measure("muc_auth_ban_check_error", "rate")
-- we will cache banned tokens to avoid extra requests
-- on destroying session, websocket retries 2 more times before giving up
@@ -38,14 +43,20 @@ end);
local function shouldAllow(session)
local token = session.auth_token;
- if token ~= nil then
- -- module:log("debug", "Checking whether user should be banned ")
-
+ if token ~= nil and session.jitsi_web_query_room and session.jitsi_web_query_prefix then
-- cached tokens are banned
if cache:get(token) then
return false;
end
+ local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
+ if not room then
+ return nil;
+ end
+ if not is_vpaas(room) then
+ return true;
+ end
+
-- TODO: do this only for enabled customers
ban_check_count();
local function cb(content, code, response, request)
@@ -54,7 +65,7 @@ local function shouldAllow(session)
local r = json.decode(content)
if r['access'] ~= nil and r['access'] == false then
module:log("info", "User is banned room:%s tenant:%s user_id:%s group:%s",
- session.jitsi_meet_room, session.jitsi_web_query_prefix,
+ session.jitsi_web_query_room, session.jitsi_web_query_prefix,
inspect(session.jitsi_meet_context_user), session.jitsi_meet_context_group);
ban_check_users_banned_count();
@@ -68,6 +79,11 @@ local function shouldAllow(session)
cache:set(token, socket.gettime());
end
+ else
+ ban_check_error_count();
+ module:log("warn", "Error code:%s contacting url:%s content:%s room:%s tenant:%s response:%s request:%s",
+ code, ACCESS_MANAGER_URL, session.jitsi_web_query_room, session.jitsi_web_query_prefix,
+ inspect(response), inspect(request), content);
end
end
diff --git a/resources/prosody-plugins/mod_muc_breakout_rooms.lua b/resources/prosody-plugins/mod_muc_breakout_rooms.lua
index ee02b0587fd2..799cd85100cd 100644
--- a/resources/prosody-plugins/mod_muc_breakout_rooms.lua
+++ b/resources/prosody-plugins/mod_muc_breakout_rooms.lua
@@ -28,6 +28,8 @@ end
local jid_node = require 'util.jid'.node;
local jid_host = require 'util.jid'.host;
local jid_split = require 'util.jid'.split;
+local jid_resource = require 'util.jid'.resource;
+local jid_bare = require 'util.jid'.bare;
local json = require 'cjson.safe';
local st = require 'util.stanza';
local uuid_gen = require 'util.uuid'.generate;
@@ -65,12 +67,16 @@ local main_muc_service;
-- Maps a breakout room jid to the main room jid
local main_rooms_map = {};
+-- Maps a full room JID to a bare connection jid for a participant that's changing rooms.
+local cache = require 'util.cache';
+local switching_room_cache = cache.new(1000);
+
-- Utility functions
function get_main_room_jid(room_jid)
local _, host = jid_split(room_jid);
- return
+ return
host == main_muc_component_config
and room_jid
or main_rooms_map[room_jid];
@@ -391,7 +397,7 @@ function on_breakout_room_pre_create(event)
end
function on_occupant_joined(event)
- local room = event.room;
+ local occupant, room = event.occupant, event.room;
if is_healthcheck_room(room.jid) then
return;
@@ -409,6 +415,9 @@ function on_occupant_joined(event)
main_room.close_timer:stop();
main_room.close_timer = nil;
end
+
+ -- clear any switching state for this occupant, we always store main room / resource
+ switching_room_cache:set(main_room_jid..'/'..jid_resource(occupant.nick), nil);
end
end
@@ -447,6 +456,11 @@ function on_occupant_pre_leave(event)
prosody.events.fire_event('jitsi-breakout-occupant-leaving', {
room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session;
});
+
+ local presence_status = stanza:get_child_text('status');
+ if presence_status == 'switch_room' then
+ switching_room_cache:set(main_room.jid..'/'..jid_resource(occupant.nick), jid_bare(occupant.jid));
+ end
end
function on_occupant_left(event)
@@ -518,6 +532,86 @@ function on_main_room_destroyed(event)
end
end
+-- Checks for a conflict with a JID in the switching_room_cache. In case of a conflict sends an error and returns true (the join is not allowed).
+-- in switching_room_cache is the same as the jid that is sending the stanza, if that is the case we can allow
+-- the join to proceed by returning false. If there is no match we send an error and return true
+-- which should halt the join.
+-- @param jid - The jid to check, this is the jid requested to join breakout or main room
+-- @param from_bare_jid - The real jid of the occupant trying to join
+-- @param room - The room being joined
+-- @param stanza - The presence stanza
+-- @param origin - The session origin to send error if needed
+function check_switching_state(jid, from_bare_jid, room, stanza, origin)
+ local switching_session_jid = switching_room_cache:get(jid);
+
+ if switching_session_jid and switching_session_jid ~= from_bare_jid then
+ local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
+ origin.send(reply);
+ return true;
+ end
+
+ return false;
+end
+
+function check_for_existing_occupant_in_room(room, requested_resource, bare_jid, stanza, origin)
+ local dest_occupant = room:get_occupant_by_nick(room.jid..'/'..requested_resource);
+ if dest_occupant ~= nil and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
+ origin.send(st.error_reply(stanza, 'cancel', 'conflict', nil, room.jid):up());
+ return true;
+ end
+end
+
+-- This is a request to join or change jid in main or breakout room. We need to check whether the requested jid does not
+-- conflict with a jid which is currently in switching state or already in another room.
+function on_occupant_pre_join_or_change(e)
+ local room, stanza, origin = e.room, e.stanza, e.origin;
+ local requested_jid = stanza.attr.to;
+
+ local main_room = get_main_room(room.jid);
+
+ -- case where the room can be destroyed while someone is switching to it
+ if not main_room then
+ origin.send(st.error_reply(stanza, 'cancel', 'service-unavailable'));
+ return true;
+ end
+
+ local main_room_requested_jid = main_room.jid..'/'..jid_resource(requested_jid);
+ local bare_jid = jid_bare(stanza.attr.from);
+
+ -- we always store main room jid with resource in switching cache
+ if check_switching_state(main_room_requested_jid, bare_jid, room, stanza, origin) then
+ return true;
+ end
+
+ if main_room == room then
+ -- this is the main room we need to check all its breakout rooms
+ for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
+ local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
+ if breakout_room then
+ if check_for_existing_occupant_in_room(
+ breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
+ return true;
+ end
+ end
+ end
+ else
+ -- this is a breakout room let's check the main room
+ if check_for_existing_occupant_in_room(main_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
+ return true;
+ end
+
+ -- now let's check the rest of the breakout rooms
+ for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
+ local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
+ if breakout_room then
+ if check_for_existing_occupant_in_room(
+ breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
+ return true;
+ end
+ end
+ end
+ end
+end
-- Module operations
@@ -544,6 +638,8 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
host_module:hook('muc-occupant-left', on_occupant_left);
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave);
+ host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
+ host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
host_module:hook('muc-disco#info', function (event)
local room = event.room;
@@ -641,6 +737,24 @@ process_host_module(breakout_rooms_muc_component_config, function(host_module, h
end
end);
+-- clears switching_room_cache on resource unbind (disconnect one way or another)
+local function handle_pre_resource_unbind(event)
+ local participant_bare_jid = jid_bare(event.jid);
+
+ -- check switching_room_cache for anyone that is switching rooms but got disconnected so we can clean up the map
+ local keysToRemove = {};
+ for key, value in switching_room_cache:items() do
+ if value == participant_bare_jid then
+ table.insert(keysToRemove, key)
+ end
+ end
+
+ for _, key in ipairs(keysToRemove) do
+ switching_room_cache:set(key, nil);
+ end
+end
+module:hook_global('jitsi-pre-session-unbind', handle_pre_resource_unbind);
+
-- operates on already loaded main muc module
function process_main_muc_loaded(main_muc, host_module)
module:log('debug', 'Main muc loaded');
@@ -650,6 +764,18 @@ function process_main_muc_loaded(main_muc, host_module)
host_module:hook('muc-occupant-joined', on_occupant_joined);
host_module:hook('muc-occupant-left', on_occupant_left);
host_module:hook('muc-room-destroyed', on_main_room_destroyed, 1); -- prosody handles it at 0
+
+ host_module:hook('muc-occupant-pre-leave', function(event)
+ local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza;
+
+ local presence_status = stanza:get_child_text('status');
+ if presence_status == 'switch_room' then
+ switching_room_cache:set(occupant.nick, jid_bare(occupant.jid));
+ end
+ end);
+
+ host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
+ host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
end
-- process or waits to process the main muc component
diff --git a/resources/prosody-plugins/mod_muc_cleanup_backend_services.lua b/resources/prosody-plugins/mod_muc_cleanup_backend_services.lua
index 94e0a282c695..b1f56bc002f7 100644
--- a/resources/prosody-plugins/mod_muc_cleanup_backend_services.lua
+++ b/resources/prosody-plugins/mod_muc_cleanup_backend_services.lua
@@ -40,13 +40,18 @@ module:hook('muc-occupant-left', function (event)
end
-- seems the room only has jibri and transcriber, add a timeout to destroy the room
+ if room.empty_destroy_timer then
+ room.empty_destroy_timer:stop();
+ end
room.empty_destroy_timer = module:add_timer(EMPTY_TIMEOUT, function()
+ if room.destroying then return end
room:destroy(nil, 'Empty room with recording and/or transcribing.');
module:log('info',
- 'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled',
- room.jid, EMPTY_TIMEOUT);
+ 'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled. By %s',
+ room.jid, EMPTY_TIMEOUT, room.empty_destroy_timer);
end)
+ module:log('info', 'Added room destroy timer %s for %s', room.empty_destroy_timer, room.jid);
end, -100); -- the last thing to execute
module:hook('muc-room-destroyed', function (event)
diff --git a/resources/prosody-plugins/mod_muc_meeting_id.lua b/resources/prosody-plugins/mod_muc_meeting_id.lua
index 6e0e3e49383e..514583ed9923 100644
--- a/resources/prosody-plugins/mod_muc_meeting_id.lua
+++ b/resources/prosody-plugins/mod_muc_meeting_id.lua
@@ -212,39 +212,39 @@ local function filterTranscriptionResult(event)
-- Do not fire the event, but forward the message
return
end
- end
-
- if msg_obj.transcript ~= nil then
- local transcription = msg_obj;
- -- in case of the string matching optimization above failed
- if transcription.is_interim then
- return;
+ if msg_obj.transcript ~= nil then
+ local transcription = msg_obj;
+
+ -- in case of the string matching optimization above failed
+ if transcription.is_interim then
+ return;
+ end
+
+ -- TODO what if we have multiple alternative transcriptions not just 1
+ local text_message = transcription.transcript[1].text;
+ --do not send empty messages
+ if text_message == '' then
+ return;
+ end
+
+ local user_id = transcription.participant.id;
+ local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
+
+ transcription.jid = who and who.jid;
+ transcription.session_id = room._data.meetingId;
+
+ local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
+ if tenant then
+ transcription.fqn = tenant..'/'..conference_name;
+ else
+ transcription.fqn = conference_name;
+ end
+ transcription.customer_id = id;
+
+ return module:fire_event('jitsi-transcript-received', {
+ room = room, occupant = occupant, transcription = transcription, stanza = stanza });
end
-
- -- TODO what if we have multiple alternative transcriptions not just 1
- local text_message = transcription.transcript[1].text;
- --do not send empty messages
- if text_message == '' then
- return;
- end
-
- local user_id = transcription.participant.id;
- local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
-
- transcription.jid = who and who.jid;
- transcription.session_id = room._data.meetingId;
-
- local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
- if tenant then
- transcription.fqn = tenant..'/'..conference_name;
- else
- transcription.fqn = conference_name;
- end
- transcription.customer_id = id;
-
- return module:fire_event('jitsi-transcript-received', {
- room = room, occupant = occupant, transcription = transcription, stanza = stanza });
end
return module:fire_event('jitsi-endpoint-message-received', {
diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts
index 4b0210002735..f420d5d78b6b 100644
--- a/tests/helpers/Participant.ts
+++ b/tests/helpers/Participant.ts
@@ -35,6 +35,8 @@ export const P1 = 'p1';
export const P2 = 'p2';
export const P3 = 'p3';
export const P4 = 'p4';
+export const P5 = 'p5';
+export const P6 = 'p6';
/**
* Participant.
@@ -112,9 +114,6 @@ export class Participant {
useStunTurn: false
},
pcStatsInterval: 1500,
- prejoinConfig: {
- enabled: false
- },
toolbarConfig: {
alwaysVisible: true
}
@@ -252,6 +251,9 @@ export class Participant {
// For the iFrame API the tenant is passed in a different way.
url = `/${options.tenant}/${url}`;
}
+ if (options.urlAppendString) {
+ url = `${url}${options.urlAppendString}`;
+ }
await this.driver.url(url);
@@ -261,6 +263,20 @@ export class Participant {
await this.switchToIFrame();
}
+ if (!options.skipPrejoinButtonClick
+ // @ts-ignore
+ && !Boolean(await this.execute(() => config.prejoinConfig?.enabled === false))) {
+ // if prejoin is enabled we want to click the join button
+ const p1PreJoinScreen = this.getPreJoinScreen();
+
+ await p1PreJoinScreen.waitForLoading();
+
+ const joinButton = p1PreJoinScreen.getJoinButton();
+
+ await joinButton.waitForDisplayed();
+ await joinButton.click();
+ }
+
if (!options.skipWaitToJoin) {
await this.waitForMucJoinedOrError();
}
@@ -674,6 +690,10 @@ export class Participant {
return new IframeAPI(this);
}
+ async getRoomMetadata() {
+ return this.execute(() => window.APP?.conference?._room?.getMetadataHandler()?.getMetadata());
+ }
+
/**
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
*/
diff --git a/tests/helpers/TestProperties.ts b/tests/helpers/TestProperties.ts
index 82aaff0ce646..ed5059eb3c3b 100644
--- a/tests/helpers/TestProperties.ts
+++ b/tests/helpers/TestProperties.ts
@@ -8,6 +8,8 @@ export type ITestProperties = {
description?: string;
/** The test requires the webhook proxy to be available. */
requireWebhookProxy: boolean;
+ /** Whether the test should be retried. */
+ retry: boolean;
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
useJaas: boolean;
/** The test uses the webhook proxy if available. */
@@ -18,6 +20,7 @@ export type ITestProperties = {
const defaultProperties: ITestProperties = {
useWebhookProxy: false,
requireWebhookProxy: false,
+ retry: false,
useJaas: false,
usesBrowsers: [ 'p1' ]
};
diff --git a/tests/helpers/WebhookProxy.ts b/tests/helpers/WebhookProxy.ts
index f8e4ebda087c..9d5179ba0b28 100644
--- a/tests/helpers/WebhookProxy.ts
+++ b/tests/helpers/WebhookProxy.ts
@@ -176,6 +176,7 @@ export default class WebhookProxy {
* @param value
*/
set defaultMeetingSettings(value: {
+ asyncTranscriptions?: boolean;
autoAudioRecording?: boolean;
autoTranscriptions?: boolean;
autoVideoRecording?: boolean;
diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts
index 8009948be1c2..ba733e02b514 100644
--- a/tests/helpers/participants.ts
+++ b/tests/helpers/participants.ts
@@ -1,4 +1,4 @@
-import { P1, P2, P3, P4, Participant } from './Participant';
+import { P1, P2, P3, P4, P5, P6, Participant } from './Participant';
import { config } from './TestsConfig';
import { IJoinOptions, IParticipantOptions } from './types';
@@ -122,6 +122,50 @@ export async function ensureFourParticipants(options?: IJoinOptions): Promise
}
+ */
+export async function ensureSixParticipants(options?: IJoinOptions): Promise {
+ await ensureOneParticipant(options);
+
+ // Join participants in batches
+ await Promise.all([
+ joinParticipant({ name: P2 }, options),
+ joinParticipant({ name: P3 }, options),
+ joinParticipant({ name: P4 }, options)
+ ]);
+
+ await Promise.all([
+ joinParticipant({ name: P5 }, options),
+ joinParticipant({ name: P6 }, options)
+ ]);
+
+ if (options?.skipInMeetingChecks) {
+ return Promise.resolve();
+ }
+
+ await Promise.all([
+ ctx.p1.waitForIceConnected(),
+ ctx.p2.waitForIceConnected(),
+ ctx.p3.waitForIceConnected(),
+ ctx.p4.waitForIceConnected(),
+ ctx.p5.waitForIceConnected(),
+ ctx.p6.waitForIceConnected()
+ ]);
+ await Promise.all([
+ ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
+ ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
+ ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
+ ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)),
+ ctx.p5.waitForSendReceiveData().then(() => ctx.p5.waitForRemoteStreams(1)),
+ ctx.p6.waitForSendReceiveData().then(() => ctx.p6.waitForRemoteStreams(1))
+ ]);
+}
+
+
/**
* Ensure that there are two participants.
*
@@ -244,10 +288,16 @@ export async function checkForScreensharingTile(sharer: Participant, observer: P
}
/**
- * Hangs up all participants (p1, p2, p3 and p4)
+ * Hangs up all participants (p1, p2, p3, p4, p5, and p6)
* @returns {Promise}
*/
export function hangupAllParticipants() {
- return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
- .map(p => p ?? Promise.resolve()));
+ return Promise.all([
+ ctx.p1?.hangup(),
+ ctx.p2?.hangup(),
+ ctx.p3?.hangup(),
+ ctx.p4?.hangup(),
+ ctx.p5?.hangup(),
+ ctx.p6?.hangup()
+ ].map(p => p ?? Promise.resolve()));
}
diff --git a/tests/helpers/types.ts b/tests/helpers/types.ts
index 397e2cfe9ae1..159cf315950a 100644
--- a/tests/helpers/types.ts
+++ b/tests/helpers/types.ts
@@ -7,13 +7,16 @@ import { IToken, ITokenOptions } from './token';
export type IContext = {
/**
- * The up-to-four browser instances provided by the framework. These can be initialized using
+ * The up-to-six browser instances provided by the framework. These can be initialized using
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
**/
p1: Participant;
p2: Participant;
p3: Participant;
p4: Participant;
+ p5: Participant;
+ p6: Participant;
+
/** A room name automatically generated by the framework for convenience. */
roomName: string;
/**
@@ -39,7 +42,7 @@ export type IParticipantOptions = {
/** Whether it should use the iFrame API. */
iFrameApi?: boolean;
/** Determines the browser instance to use. */
- name: 'p1' | 'p2' | 'p3' | 'p4';
+ name: 'p1' | 'p2' | 'p3' | 'p4' | 'p5' | 'p6';
/** An optional token to use. */
token?: IToken;
};
@@ -62,6 +65,11 @@ export type IParticipantJoinOptions = {
*/
skipDisplayName?: boolean;
+ /**
+ * Whether to skip the prejoin button 'Join' click when joining.
+ */
+ skipPrejoinButtonClick?: boolean;
+
/**
* Whether to skip waiting for the participant to join the room. Cases like lobby where we do not succeed to join
* based on the logic of the test.
@@ -72,6 +80,11 @@ export type IParticipantJoinOptions = {
* An optional tenant to use. If provided it overrides the default.
*/
tenant?: string;
+
+ /**
+ * An optional string to append to the URL when joining the room.
+ */
+ urlAppendString?: string;
};
export type IJoinOptions = {
@@ -96,6 +109,11 @@ export type IJoinOptions = {
*/
skipInMeetingChecks?: boolean;
+ /**
+ * Whether to skip the prejoin button 'Join' click when joining.
+ */
+ skipPrejoinButtonClick?: boolean;
+
/**
* The skip waiting for the participant to join the room setting to pass to IParticipantJoinOptions.
*/
@@ -110,4 +128,9 @@ export type IJoinOptions = {
* Options used when generating a token.
*/
tokenOptions?: ITokenOptions;
+
+ /**
+ * An optional string to append to the URL when joining the room.
+ */
+ urlAppendString?: string;
};
diff --git a/tests/pageobjects/Notifications.ts b/tests/pageobjects/Notifications.ts
index e0049beedb31..eb04e60394c5 100644
--- a/tests/pageobjects/Notifications.ts
+++ b/tests/pageobjects/Notifications.ts
@@ -2,6 +2,7 @@ import BasePageObject from './BasePageObject';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
+const AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectVideoTitle';
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
@@ -57,6 +58,13 @@ export default class Notifications extends BasePageObject {
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
}
+ /**
+ * Closes the video muted notification.
+ */
+ async closeAVModerationVideoMutedNotification(skipNonExisting = false) {
+ return this.closeNotification(AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID, skipNonExisting);
+ }
+
/**
* Closes the ask to unmute notification.
*/
@@ -106,14 +114,22 @@ export default class Notifications extends BasePageObject {
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
- const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
+ const closeButton = this.participant.driver.$(`[data-testid="${testId}"] #close-notification`);
- if (skipNonExisting && !await closeButton.isExisting()) {
- return Promise.resolve();
- }
+ try {
+ if (skipNonExisting && !await closeButton.isExisting()) {
+ return Promise.resolve();
+ }
- await closeButton.moveTo();
- await closeButton.click();
+ await closeButton.moveTo();
+ await closeButton.click();
+ } catch (e) {
+ console.error(`Error closing notification ${testId}`, e);
+
+ if (!skipNonExisting) {
+ throw e;
+ }
+ }
}
/**
diff --git a/tests/pageobjects/PasswordDialog.ts b/tests/pageobjects/PasswordDialog.ts
index 540b38653cf3..74edad2c1a40 100644
--- a/tests/pageobjects/PasswordDialog.ts
+++ b/tests/pageobjects/PasswordDialog.ts
@@ -40,7 +40,7 @@ export default class PasswordDialog extends BaseDialog {
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
await passwordInput.waitForExist();
- await passwordInput.waitForClickable({ timeout: 2000 });
+ await passwordInput.waitForStable();
await passwordInput.click();
await passwordInput.clearValue();
diff --git a/tests/specs/helpers/mute.ts b/tests/specs/helpers/mute.ts
index d1bb67b14bf1..8519882979c7 100644
--- a/tests/specs/helpers/mute.ts
+++ b/tests/specs/helpers/mute.ts
@@ -45,6 +45,8 @@ export async function unmuteAudioAndCheck(testee: Participant, observer: Partici
* @param observer
*/
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise {
+ await testee.getNotifications().closeAskToUnmuteNotification(true);
+ await testee.getNotifications().closeAVModerationVideoMutedNotification(true);
await testee.getToolbar().clickVideoUnmuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
diff --git a/tests/specs/jaas/recording.spec.ts b/tests/specs/jaas/recording.spec.ts
index 315001646687..7abef12b5434 100644
--- a/tests/specs/jaas/recording.spec.ts
+++ b/tests/specs/jaas/recording.spec.ts
@@ -7,6 +7,7 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
requireWebhookProxy: true,
+ retry: true,
useJaas: true
});
diff --git a/tests/specs/jaas/transcriptions.spec.ts b/tests/specs/jaas/transcriptions.spec.ts
index 4fc049fc16ae..c320260aa432 100644
--- a/tests/specs/jaas/transcriptions.spec.ts
+++ b/tests/specs/jaas/transcriptions.spec.ts
@@ -8,182 +8,201 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
requireWebhookProxy: true,
+ retry: true,
useJaas: true,
usesBrowsers: [ 'p1', 'p2' ]
});
-describe('Transcription', () => {
- let p1: Participant, p2: Participant;
- let webhooksProxy: WebhookProxy;
+for (const asyncTranscriptions of [ false, true ]) {
+ describe(`Transcription (async=${asyncTranscriptions})`, () => {
+ let p1: Participant, p2: Participant;
+ let webhooksProxy: WebhookProxy;
- it('setup', async () => {
- const room = ctx.roomName;
+ it('setup', async () => {
+ const room = ctx.roomName;
- webhooksProxy = ctx.webhooksProxy;
+ webhooksProxy = ctx.webhooksProxy;
+ webhooksProxy.defaultMeetingSettings = { asyncTranscriptions };
- p1 = await joinJaasMuc({
- name: 'p1',
- token: t({ room, moderator: true }),
- iFrameApi: true });
+ p1 = await joinJaasMuc({
+ name: 'p1',
+ token: t({ room, moderator: true }),
+ iFrameApi: true
+ });
- const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
+ const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
- expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
+ expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
- p2 = await joinJaasMuc({
- name: 'p2',
- token: t({ room }),
- iFrameApi: true }, {
- configOverwrite: {
- startWithAudioMuted: true
- }
- });
+ const roomMetadata = await p1.getRoomMetadata();
- await Promise.all([
- p1.switchToMainFrame(),
- p2.switchToMainFrame(),
- ]);
+ if (asyncTranscriptions) {
+ expect(roomMetadata.asyncTranscription).toBe(true);
+ } else {
+ expect(roomMetadata.asyncTranscription).toBeFalsy();
+ }
- expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
- expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
- });
+ p2 = await joinJaasMuc({
+ name: 'p2',
+ token: t({ room }),
+ iFrameApi: true
+ }, {
+ configOverwrite: {
+ startWithAudioMuted: true
+ }
+ });
+
+ await Promise.all([
+ p1.switchToMainFrame(),
+ p2.switchToMainFrame(),
+ ]);
+
+ expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
+ expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
+ });
- it('toggle subtitles', async () => {
- await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
- await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
- await p1.getIframeAPI().executeCommand('toggleSubtitles');
+ it('toggle subtitles', async () => {
+ await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
+ await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
+ await p1.getIframeAPI().executeCommand('toggleSubtitles');
- await checkReceivingChunks(p1, p2, webhooksProxy);
+ await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
- await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
- await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
+ await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
+ await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
- await p1.getIframeAPI().executeCommand('toggleSubtitles');
+ await p1.getIframeAPI().executeCommand('toggleSubtitles');
- await p1.driver.waitUntil(() => p1.getIframeAPI()
- .getEventResult('transcribingStatusChanged'), {
- timeout: 15000,
- timeoutMsg: 'transcribingStatusChanged event not received by p1'
+ await p1.driver.waitUntil(() => p1.getIframeAPI()
+ .getEventResult('transcribingStatusChanged'), {
+ timeout: 15000,
+ timeoutMsg: 'transcribingStatusChanged event not received by p1'
+ });
});
- });
- it('set subtitles on and off', async () => {
- // we need to clear results or the last one will be used, from the previous time subtitles were on
- await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
- await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
+ it('set subtitles on and off', async () => {
+ // we need to clear results or the last one will be used, from the previous time subtitles were on
+ await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
+ await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
- await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
+ await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
- await checkReceivingChunks(p1, p2, webhooksProxy);
+ await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
- await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
+ await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
- await p1.getIframeAPI().executeCommand('setSubtitles', false);
+ await p1.getIframeAPI().executeCommand('setSubtitles', false);
- await p1.driver.waitUntil(() => p1.getIframeAPI()
- .getEventResult('transcribingStatusChanged'), {
- timeout: 15000,
- timeoutMsg: 'transcribingStatusChanged event not received by p1'
+ await p1.driver.waitUntil(() => p1.getIframeAPI()
+ .getEventResult('transcribingStatusChanged'), {
+ timeout: 15000,
+ timeoutMsg: 'transcribingStatusChanged event not received by p1'
+ });
});
- });
- it('start/stop transcriptions via recording', async () => {
- // we need to clear results or the last one will be used, from the previous time subtitles were on
- await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
- await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
- await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
+ it('start/stop transcriptions via recording', async () => {
+ // we need to clear results or the last one will be used, from the previous time subtitles were on
+ await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
+ await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
+ await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
- await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
+ await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
- await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
+ await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
- let allTranscriptionStatusChanged: Promise[] = [];
+ let allTranscriptionStatusChanged: Promise[] = [];
- allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
+ allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
- timeout: 10000,
- timeoutMsg: 'transcribingStatusChanged event not received on p1'
- }));
- allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
+ timeout: 10000,
+ timeoutMsg: 'transcribingStatusChanged event not received on p1'
+ }));
+ allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
- timeout: 10000,
- timeoutMsg: 'transcribingStatusChanged event not received on p2'
- }));
+ timeout: 10000,
+ timeoutMsg: 'transcribingStatusChanged event not received on p2'
+ }));
- let result = await Promise.allSettled(allTranscriptionStatusChanged);
+ let result = await Promise.allSettled(allTranscriptionStatusChanged);
- expect(result.length).toBe(2);
+ expect(result.length).toBe(2);
- result.forEach(e => {
- // @ts-ignore
- expect(e.value.on).toBe(true);
- });
+ result.forEach(e => {
+ // @ts-ignore
+ expect(e.value.on).toBe(true);
+ });
- await checkReceivingChunks(p1, p2, webhooksProxy);
+ await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
- await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
- await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
+ await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
+ await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
- await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
+ await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
- allTranscriptionStatusChanged = [];
+ allTranscriptionStatusChanged = [];
- allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
- .getEventResult('transcribingStatusChanged'), {
- timeout: 10000,
- timeoutMsg: 'transcribingStatusChanged event not received on p1'
- }));
- allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
- .getEventResult('transcribingStatusChanged'), {
- timeout: 10000,
- timeoutMsg: 'transcribingStatusChanged event not received on p2'
- }));
+ allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
+ .getEventResult('transcribingStatusChanged'), {
+ timeout: 10000,
+ timeoutMsg: 'transcribingStatusChanged event not received on p1'
+ }));
+ allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
+ .getEventResult('transcribingStatusChanged'), {
+ timeout: 10000,
+ timeoutMsg: 'transcribingStatusChanged event not received on p2'
+ }));
- result = await Promise.allSettled(allTranscriptionStatusChanged);
+ result = await Promise.allSettled(allTranscriptionStatusChanged);
- expect(result.length).toBe(2);
+ expect(result.length).toBe(2);
- result.forEach(e => {
- // @ts-ignore
- expect(e.value.on).toBe(false);
- });
+ result.forEach(e => {
+ // @ts-ignore
+ expect(e.value.on).toBe(false);
+ });
- await p1.getIframeAPI().executeCommand('hangup');
- await p2.getIframeAPI().executeCommand('hangup');
+ await p1.getIframeAPI().executeCommand('hangup');
+ await p2.getIframeAPI().executeCommand('hangup');
- // sometimes events are not immediately received,
- // let's wait for destroy event before waiting for those that depends on it
- await webhooksProxy.waitForEvent('ROOM_DESTROYED');
+ // sometimes events are not immediately received,
+ // let's wait for destroy event before waiting for those that depends on it
+ await webhooksProxy.waitForEvent('ROOM_DESTROYED');
- const event: {
- data: {
- preAuthenticatedLink: string;
- };
- eventType: string;
- } = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
+ const event: {
+ data: {
+ preAuthenticatedLink: string;
+ };
+ eventType: string;
+ } = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
- expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
- expect(event.data.preAuthenticatedLink).toBeDefined();
+ expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
+ expect(event.data.preAuthenticatedLink).toBeDefined();
+ });
});
-});
-
-async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy) {
- const allTranscripts: Promise[] = [];
+}
- allTranscripts.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
+/**
+ *
+ * @param p1
+ * @param p2
+ * @param webhooksProxy
+ * @param expectName Whether to expect the events to contain the name of the participant. Currently, async
+ * transcriptions do not include the name. TODO: remove this parameter when async transcription events are fixed.
+ */
+async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, expectName = true) {
+ const p1Promise = p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcriptionChunkReceived'), {
timeout: 60000,
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
- }));
+ });
- allTranscripts.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
+ const p2Promise = p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcriptionChunkReceived'), {
timeout: 60000,
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
- }));
+ });
- // TRANSCRIPTION_CHUNK_RECEIVED webhook
- allTranscripts.push((async () => {
+ const webhookPromise = async () => {
const event: {
data: {
final: string;
@@ -198,42 +217,36 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
eventType: string;
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
- expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
+ expect(event.eventType).toBe('TRANSCRIPTION_CHUNK_RECEIVED');
event.data.stable = event.data.final;
return event;
- })());
-
- const result = await Promise.allSettled(allTranscripts);
+ };
- expect(result.length).toBeGreaterThan(0);
-
- // @ts-ignore
- const firstEntryData = result[0].value.data;
- const stable = firstEntryData.stable || firstEntryData.final;
- const language = firstEntryData.language;
- const messageID = firstEntryData.messageID;
+ const [ p1Event, p2Event, webhookEvent ] = await Promise.all([ p1Promise, p2Promise, await webhookPromise() ]);
const p1Id = await p1.getEndpointId();
- result.map(r => {
- // @ts-ignore
- const v = r.value;
+ const p1Transcript = p1Event.data.stable || p1Event.data.final;
+ const p2Transcript = p2Event.data.stable || p2Event.data.final;
+ const webhookTranscript = webhookEvent.data.stable || webhookEvent.data.final;
- expect(v).toBeDefined();
+ expect(p2Transcript.includes(p1Transcript) || p1Transcript.includes(p2Transcript)).toBe(true);
+ expect(webhookTranscript.includes(p1Transcript) || p1Transcript.includes(webhookTranscript)).toBe(true);
- return v.data;
- }).forEach(tr => {
- const checkTranscripts = stable.includes(tr.stable || tr.final) || (tr.stable || tr.final).includes(stable);
+ expect(p2Event.data.language).toBe(p1Event.data.language);
+ expect(webhookEvent.data.language).toBe(p1Event.data.language);
- if (!checkTranscripts) {
- console.log('received events', JSON.stringify(result));
- }
+ expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
+ expect(webhookEvent.data.messageID).toBe(p1Event.data.messageID);
- expect(checkTranscripts).toBe(true);
- expect(tr.language).toBe(language);
- expect(tr.messageID).toBe(messageID);
- expect(tr.participant.id).toBe(p1Id);
- expect(tr.participant.name).toBe(p1.name);
- });
+ expect(p1Event.data.participant.id).toBe(p1Id);
+ expect(p2Event.data.participant.id).toBe(p1Id);
+ expect(webhookEvent.data.participant.id).toBe(p1Id);
+
+ if (expectName) {
+ expect(p1Event.data.participant.name).toBe(p1.name);
+ expect(p2Event.data.participant.name).toBe(p1.name);
+ expect(webhookEvent.data.participant.name).toBe(p1.name);
+ }
}
diff --git a/tests/specs/media/activeSpeaker.spec.ts b/tests/specs/media/activeSpeaker.spec.ts
index 8a59fe2cb237..7451cfb3e6c3 100644
--- a/tests/specs/media/activeSpeaker.spec.ts
+++ b/tests/specs/media/activeSpeaker.spec.ts
@@ -1,16 +1,20 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
-import { ensureThreeParticipants } from '../../helpers/participants';
+import {
+ checkForScreensharingTile,
+ ensureSixParticipants,
+ ensureThreeParticipants,
+ hangupAllParticipants
+} from '../../helpers/participants';
import { muteAudioAndCheck } from '../helpers/mute';
setTestProperties(__filename, {
- usesBrowsers: [ 'p1', 'p2', 'p3' ]
+ usesBrowsers: [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ]
});
describe('Active speaker', () => {
it('testActiveSpeaker', async () => {
await ensureThreeParticipants();
-
const { p1, p2, p3 } = ctx;
await muteAudioAndCheck(p1, p2);
@@ -30,6 +34,306 @@ describe('Active speaker', () => {
await assertOneDominantSpeaker(p1);
await assertOneDominantSpeaker(p2);
await assertOneDominantSpeaker(p3);
+
+ await hangupAllParticipants();
+ });
+
+ /**
+ * Test that the dominant speaker appears in the filmstrip in stage view
+ * even when alphabetically last with limited visible slots.
+ * This tests the fix for the bug where dominant speakers at the bottom of
+ * the alphabetically sorted list would not appear when slots were limited.
+ *
+ * Note: This test verifies filmstrip ordering via Redux state
+ * (visibleRemoteParticipants), not large video behavior.
+ */
+ it.skip('testDominantSpeakerInFilmstripWithLimitedSlots', async () => {
+ await ensureSixParticipants({
+ configOverwrite: {
+ startWithAudioMuted: true
+ }
+ });
+ const { p1, p2, p3, p4, p5, p6 } = ctx;
+
+ // Resize p1's window to limit filmstrip slots to 2-3 tiles
+ // This creates the condition where not all participants fit in the filmstrip
+ await p1.driver.setWindowSize(1024, 600);
+ await p1.driver.pause(1000); // Wait for layout to adjust
+
+ // Set display names to create alphabetical ordering
+ // Names chosen so p6 ("Zoe") is alphabetically last: Alice, Bob, Charlie, Eve, Frank, Zoe
+ await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
+
+ // Test with multiple speakers: Eve (p4), Frank (p5), and Zoe (p6)
+ // This verifies the fix works for different alphabetical positions
+ const speakersToTest = [
+ { participant: p4, name: 'Eve' },
+ { participant: p5, name: 'Frank' },
+ { participant: p6, name: 'Zoe' }
+ ];
+
+ for (const { participant, name } of speakersToTest) {
+ const participantId = await participant.getEndpointId();
+
+ await p1.log(`Testing ${name} (${participantId}) as dominant speaker`);
+
+ // Make this participant the dominant speaker by unmuting
+ await participant.getToolbar().clickAudioUnmuteButton();
+
+ // Wait for the dominant speaker to be detected
+ await waitForDominantSpeaker(p1, participantId, name);
+
+ // Verify that the participant appears in the visible remote participants
+ const filmstripState = await getFilmstripState(p1);
+
+ await p1.log(`Dominant speaker: ${filmstripState.dominantSpeaker}`);
+ await p1.log(`Visible remote participants: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
+ await p1.log(`${name} endpoint ID: ${participantId}`);
+ await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
+
+ // Verify we actually have slot limitation (fewer visible than total)
+ expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
+
+ // Assert that the dominant speaker is in the visible participants
+ // This is the key test - even though they may be alphabetically late and slots are limited,
+ // they should still be visible because the fix reserves a slot for dominant speaker
+ expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
+
+ // Verify the dominant speaker thumbnail is visible in the filmstrip
+ await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
+ timeout: 5_000,
+ timeoutMsg: `${name} dominant speaker thumbnail not visible in filmstrip`
+ });
+
+ // Mute this participant back before testing the next one
+ await participant.getToolbar().clickAudioMuteButton();
+ await p1.driver.pause(2000);
+ }
+
+ await hangupAllParticipants();
+ });
+
+ /**
+ * Test dominant speaker in filmstrip with screensharing active.
+ * Verifies that dominant speaker is still visible when screen shares
+ * take up some of the visible slots.
+ */
+ it.skip('testDominantSpeakerWithScreensharing', async () => {
+ await ensureSixParticipants({
+ configOverwrite: {
+ startWithAudioMuted: true
+ }
+ });
+ const { p1, p2, p3, p4, p5, p6 } = ctx;
+
+ // Resize p1's window to limit filmstrip slots
+ await p1.driver.setWindowSize(1024, 600);
+ await p1.driver.pause(1000); // Wait for layout to adjust
+
+ // Set display names
+ await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
+
+ // Start screensharing from p2
+ await p2.getToolbar().clickDesktopSharingButton();
+ await checkForScreensharingTile(p2, p1);
+
+ // Test with multiple speakers while screensharing is active
+ const speakersToTest = [
+ { participant: p4, name: 'Eve' },
+ { participant: p5, name: 'Frank' },
+ { participant: p6, name: 'Zoe' }
+ ];
+
+ for (const { participant, name } of speakersToTest) {
+ const participantId = await participant.getEndpointId();
+
+ await p1.log(`Testing ${name} (${participantId}) as dominant speaker with screensharing`);
+
+ // Make this participant the dominant speaker by unmuting
+ await participant.getToolbar().clickAudioUnmuteButton();
+
+ // Wait for the dominant speaker to be detected
+ await waitForDominantSpeaker(p1, participantId, name);
+
+ // Verify dominant speaker is still visible in filmstrip despite screenshare
+ const filmstripState = await getFilmstripState(p1);
+
+ await p1.log(`Dominant speaker (with screenshare): ${filmstripState.dominantSpeaker}`);
+ await p1.log(`Visible remote participants with screenshare: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
+ await p1.log(`${name} endpoint ID: ${participantId}`);
+ await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
+
+ // Verify we have slot limitation even with screensharing
+ expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
+
+ // The dominant speaker should still be in the visible participants despite screenshare taking slots
+ expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
+
+ // Verify thumbnail visibility
+ await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
+ timeout: 5_000,
+ timeoutMsg: `${name} not visible with screensharing active`
+ });
+
+ // Mute this participant back before testing the next one
+ await participant.getToolbar().clickAudioMuteButton();
+ await p1.driver.pause(1000);
+ }
+
+ // Clean up - stop screensharing
+ await p2.getToolbar().clickStopDesktopSharingButton();
+
+ await hangupAllParticipants();
+ });
+
+ /**
+ * Test that filmstrip maintains stable ordering when multiple speakers alternate.
+ * Verifies that the alphabetical sorting prevents visual reordering when the same
+ * set of speakers take turns speaking.
+ */
+ it.skip('testFilmstripStableOrderingWithMultipleSpeakers', async () => {
+ await ensureSixParticipants({
+ configOverwrite: {
+ startWithAudioMuted: true
+ }
+ });
+ const { p1, p2, p3, p4, p5, p6 } = ctx;
+
+ // Resize p1's window to limit filmstrip slots
+ await p1.driver.setWindowSize(1024, 600);
+ await p1.driver.pause(1000); // Wait for layout to adjust
+
+ // Set display names
+ await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
+
+ // First, have Eve, Frank, and Zoe all speak to get them into the active speakers list
+ const speakersToTest = [
+ { participant: p4, name: 'Eve' },
+ { participant: p5, name: 'Frank' },
+ { participant: p6, name: 'Zoe' }
+ ];
+
+ await p1.log('Initial round: getting all three speakers into active speakers list');
+
+ for (const { participant, name } of speakersToTest) {
+ const participantId = await participant.getEndpointId();
+
+ await p1.log(`${name} (${participantId}) speaking for the first time`);
+ await participant.getToolbar().clickAudioUnmuteButton();
+ await waitForDominantSpeaker(p1, participantId, name);
+ await participant.getToolbar().clickAudioMuteButton();
+ await p1.driver.pause(1000);
+ }
+
+ // Now cycle through them again and verify they maintain alphabetical order (Eve, Frank, Zoe)
+ await p1.log('Second round: verifying stable alphabetical ordering when speakers alternate');
+
+ const states = [];
+
+ for (const { participant, name } of speakersToTest) {
+ const participantId = await participant.getEndpointId();
+
+ await p1.log(`Testing ${name} (${participantId}) for stable ordering`);
+
+ // Make this participant the dominant speaker
+ await participant.getToolbar().clickAudioUnmuteButton();
+
+ // Wait for the dominant speaker to be detected
+ await waitForDominantSpeaker(p1, participantId, name);
+
+ // Capture filmstrip state
+ const filmstripState = await getFilmstripState(p1);
+
+ states.push({ name, id: participantId, state: filmstripState });
+
+ // Mute back
+ await participant.getToolbar().clickAudioMuteButton();
+ await p1.driver.pause(1000);
+ }
+
+ const [ eveState, frankState, zoeState ] = states;
+
+ // Helper function to get participant names in the order they appear
+ const getVisibleParticipantNames = async (visibleIds: string[]) => {
+ return await p1.execute(ids => {
+ const state = APP.store.getState();
+ const participants = state['features/base/participants'];
+
+ return ids.map(id => {
+ const participant = participants.remote.get(id);
+
+ return participant?.name || 'Unknown';
+ });
+ }, visibleIds);
+ };
+
+ // Get the names of visible participants for each state
+ const eveVisibleNames = await getVisibleParticipantNames(eveState.state.visibleRemoteParticipants);
+ const frankVisibleNames = await getVisibleParticipantNames(frankState.state.visibleRemoteParticipants);
+ const zoeVisibleNames = await getVisibleParticipantNames(zoeState.state.visibleRemoteParticipants);
+
+ await p1.log(`Visible participants when Eve is dominant: ${JSON.stringify(eveVisibleNames)}`);
+ await p1.log(`Visible participants when Frank is dominant: ${JSON.stringify(frankVisibleNames)}`);
+ await p1.log(`Visible participants when Zoe is dominant: ${JSON.stringify(zoeVisibleNames)}`);
+
+ await p1.log(`Eve visible count: ${eveState.state.visibleRemoteParticipants.length}, total remote: ${eveState.state.remoteParticipants.length}`);
+ await p1.log(`Frank visible count: ${frankState.state.visibleRemoteParticipants.length}, total remote: ${frankState.state.remoteParticipants.length}`);
+ await p1.log(`Zoe visible count: ${zoeState.state.visibleRemoteParticipants.length}, total remote: ${zoeState.state.remoteParticipants.length}`);
+
+ // Verify that each dominant speaker appears in visible participants
+ expect(eveState.state.visibleRemoteParticipants).toContain(eveState.id);
+ expect(frankState.state.visibleRemoteParticipants).toContain(frankState.id);
+ expect(zoeState.state.visibleRemoteParticipants).toContain(zoeState.id);
+
+ // Helper function to get the relative order of Eve, Frank, and Zoe
+ const getSpeakersOrder = (names: string[]) => {
+ return names.filter(n => [ 'Eve', 'Frank', 'Zoe' ].includes(n));
+ };
+
+ const eveOrder = getSpeakersOrder(eveVisibleNames);
+ const frankOrder = getSpeakersOrder(frankVisibleNames);
+ const zoeOrder = getSpeakersOrder(zoeVisibleNames);
+
+ await p1.log(`Speakers order when Eve is dominant: ${JSON.stringify(eveOrder)}`);
+ await p1.log(`Speakers order when Frank is dominant: ${JSON.stringify(frankOrder)}`);
+ await p1.log(`Speakers order when Zoe is dominant: ${JSON.stringify(zoeOrder)}`);
+
+ // Verify that the dominant speaker is always in the visible list (this tests the bug fix)
+ expect(eveOrder).toContain('Eve');
+ expect(frankOrder).toContain('Frank');
+ expect(zoeOrder).toContain('Zoe');
+
+ // Helper to check if array is alphabetically sorted
+ const isAlphabeticallySorted = (names: string[]) => {
+ for (let i = 0; i < names.length - 1; i++) {
+ if (names[i].localeCompare(names[i + 1]) > 0) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+
+ // Verify that whatever speakers ARE visible maintain alphabetical order
+ // This is the key test - when the same speakers alternate, visible speakers stay in alphabetical order
+ expect(isAlphabeticallySorted(eveOrder)).toBe(true);
+ expect(isAlphabeticallySorted(frankOrder)).toBe(true);
+ expect(isAlphabeticallySorted(zoeOrder)).toBe(true);
+
+ // Additionally verify order consistency: if multiple speakers are visible in multiple states,
+ // their relative order should be the same
+ // For example, if Eve and Frank are both visible when Zoe speaks, they should be [Eve, Frank]
+ if (eveOrder.includes('Frank') && frankOrder.includes('Eve')) {
+ // Both Eve and Frank visible in both states
+ const eveAndFrankInEveState = eveOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
+ const eveAndFrankInFrankState = frankOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
+
+ expect(eveAndFrankInEveState).toEqual(eveAndFrankInFrankState);
+ }
+
+ await p1.log('Filmstrip maintains alphabetical ordering of visible speakers when dominant speaker changes');
+
+ await hangupAllParticipants();
});
});
@@ -95,3 +399,81 @@ async function assertOneDominantSpeaker(participant: Participant) {
expect(await participant.driver.$$(
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
}
+
+/**
+ * Wait for a participant to be detected as the dominant speaker.
+ *
+ * @param {Participant} observer - The participant observing the dominant speaker state.
+ * @param {string} participantId - The endpoint ID of the expected dominant speaker.
+ * @param {string} participantName - The name of the participant for logging.
+ */
+async function waitForDominantSpeaker(
+ observer: Participant,
+ participantId: string,
+ participantName: string
+): Promise {
+ await observer.driver.waitUntil(
+ async () => {
+ const state = await observer.execute(() => {
+ const participants = APP.store.getState()['features/base/participants'];
+
+ return participants.dominantSpeaker;
+ });
+
+ return state === participantId;
+ },
+ {
+ timeout: 10000,
+ timeoutMsg: `${participantName} (${participantId}) was not detected as dominant speaker within 10 seconds`
+ }
+ );
+
+ // Wait a bit more for filmstrip state to update after dominant speaker changes
+ await observer.driver.pause(1000);
+}
+
+/**
+ * Get the current filmstrip state from Redux.
+ *
+ * @param {Participant} participant - The participant to query.
+ * @returns {Promise} The filmstrip state.
+ */
+async function getFilmstripState(participant: Participant): Promise<{
+ dominantSpeaker: string | null;
+ remoteParticipants: string[];
+ visibleRemoteParticipants: string[];
+}> {
+ return await participant.execute(() => {
+ const state = APP.store.getState();
+ const filmstrip = state['features/filmstrip'];
+ const participants = state['features/base/participants'];
+
+ return {
+ dominantSpeaker: participants.dominantSpeaker,
+ remoteParticipants: filmstrip.remoteParticipants,
+ visibleRemoteParticipants: Array.from(filmstrip.visibleRemoteParticipants)
+ };
+ });
+}
+
+/**
+ * Set display names for all 6 participants to create alphabetical ordering.
+ */
+async function setAlphabeticalDisplayNames(
+ p1: Participant,
+ p2: Participant,
+ p3: Participant,
+ p4: Participant,
+ p5: Participant,
+ p6: Participant
+): Promise {
+ await p1.setLocalDisplayName('Alice');
+ await p2.setLocalDisplayName('Bob');
+ await p3.setLocalDisplayName('Charlie');
+ await p4.setLocalDisplayName('Eve');
+ await p5.setLocalDisplayName('Frank');
+ await p6.setLocalDisplayName('Zoe');
+
+ // Wait for display names to propagate
+ await p1.driver.pause(2000);
+}
diff --git a/tests/specs/media/audioVideoModeration.spec.ts b/tests/specs/media/audioVideoModeration.spec.ts
index 85aae63263e6..359683ee8d9d 100644
--- a/tests/specs/media/audioVideoModeration.spec.ts
+++ b/tests/specs/media/audioVideoModeration.spec.ts
@@ -122,12 +122,12 @@ describe('Audio/video moderation', () => {
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
- await moderatorParticipantsPane.allowVideo(nonModerator);
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
-
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
-
await unmuteAudioAndCheck(nonModerator, p1);
+
+ await moderatorParticipantsPane.allowVideo(nonModerator);
+ await nonModerator.getNotifications().waitForAskToUnmuteNotification();
await unmuteVideoAndCheck(nonModerator, p1);
await moderatorParticipantsPane.clickContextMenuButton();
@@ -190,6 +190,10 @@ describe('Audio/video moderation', () => {
// stop video and check
await p1.getFilmstrip().muteVideo(p2);
+ // close and open participants pane to make sure the context menu disappears
+ await p1.getParticipantsPane().close();
+ await p1.getParticipantsPane().open();
+
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
@@ -256,11 +260,12 @@ async function unmuteByModerator(
await moderator.getNotifications().waitForRaisedHandNotification();
// ask participant to unmute
- await moderatorParticipantsPane.allowVideo(participant);
await moderatorParticipantsPane.askToUnmute(participant, false);
await participant.getNotifications().waitForAskToUnmuteNotification();
-
await unmuteAudioAndCheck(participant, moderator);
+
+ await moderatorParticipantsPane.allowVideo(participant);
+ await participant.getNotifications().waitForAskToUnmuteNotification();
await unmuteVideoAndCheck(participant, moderator);
if (stopModeration) {
diff --git a/tests/specs/media/desktopSharing.spec.ts b/tests/specs/media/desktopSharing.spec.ts
index b165c90921a8..522d109a1808 100644
--- a/tests/specs/media/desktopSharing.spec.ts
+++ b/tests/specs/media/desktopSharing.spec.ts
@@ -183,7 +183,8 @@ describe('Desktop sharing', () => {
p2p: {
backToP2PDelay: 3,
enabled: true
- }
+ },
+ startWithAudioMuted: false
}
});
const { p1 } = ctx;
@@ -201,7 +202,8 @@ describe('Desktop sharing', () => {
p2p: {
backToP2PDelay: 3,
enabled: true
- }
+ },
+ startWithAudioMuted: false
}
});
const { p2, p3 } = ctx;
@@ -304,11 +306,16 @@ describe('Desktop sharing', () => {
await ensureTwoParticipants({
configOverwrite: {
- startWithAudioMuted: true
+ startWithAudioMuted: true,
+ startWithVideoMuted: false
},
skipInMeetingChecks: true
});
await ensureThreeParticipants({
+ configOverwrite: {
+ startWithAudioMuted: false,
+ startWithVideoMuted: false
+ },
skipInMeetingChecks: true
});
const { p2, p3 } = ctx;
@@ -334,7 +341,12 @@ describe('Desktop sharing', () => {
it('with lastN', async () => {
await hangupAllParticipants();
- await ensureThreeParticipants();
+ await ensureThreeParticipants({
+ configOverwrite: {
+ startWithAudioMuted: false,
+ startWithVideoMuted: false
+ },
+ });
const { p1, p2, p3 } = ctx;
await p3.getToolbar().clickDesktopSharingButton();
@@ -345,7 +357,8 @@ describe('Desktop sharing', () => {
await ensureFourParticipants({
configOverwrite: {
channelLastN: 2,
- startWithAudioMuted: true
+ startWithAudioMuted: true,
+ startWithVideoMuted: false
}
});
const { p4 } = ctx;
diff --git a/tests/specs/media/prejoinDisabled.spec.ts b/tests/specs/media/prejoinDisabled.spec.ts
new file mode 100644
index 000000000000..d6cabfcb4b60
--- /dev/null
+++ b/tests/specs/media/prejoinDisabled.spec.ts
@@ -0,0 +1,44 @@
+import type { Participant } from '../../helpers/Participant';
+import { setTestProperties } from '../../helpers/TestProperties';
+import { ensureTwoParticipants, hangupAllParticipants } from '../../helpers/participants';
+
+setTestProperties(__filename, {
+ usesBrowsers: [ 'p1', 'p2' ]
+});
+
+describe('Prejoin disabled', () => {
+ it('joining with no prejoin - nested object', async () => {
+ await ensureTwoParticipants({
+ configOverwrite: {
+ prejoinConfig: {
+ enabled: false
+ }
+ },
+ skipInMeetingChecks: true,
+ });
+
+ await checkEveryoneMuted(ctx);
+
+ await hangupAllParticipants();
+ });
+ it('joining with no prejoin - direct url param ', async () => {
+ await ensureTwoParticipants({
+ skipInMeetingChecks: true,
+ urlAppendString: '&config.prejoinConfig.enabled=false'
+ });
+
+ await checkEveryoneMuted(ctx);
+ });
+});
+
+async function checkEveryoneMuted({ p1, p2 }: { p1: Participant; p2: Participant; }) {
+ await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
+ await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
+ await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
+ await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
+
+ await p1.getParticipantsPane().assertAudioMuteIconIsDisplayed(p2);
+ await p1.getParticipantsPane().assertAudioMuteIconIsDisplayed(p1);
+ await p2.getParticipantsPane().assertAudioMuteIconIsDisplayed(p2);
+ await p2.getParticipantsPane().assertAudioMuteIconIsDisplayed(p1);
+}
diff --git a/tests/specs/media/startMuted.spec.ts b/tests/specs/media/startMuted.spec.ts
index b9bf5a852353..5c5bffcd0135 100644
--- a/tests/specs/media/startMuted.spec.ts
+++ b/tests/specs/media/startMuted.spec.ts
@@ -190,6 +190,7 @@ describe('Start muted', () => {
const options = {
configOverwrite: {
startWithAudioMuted: true,
+ startWithVideoMuted: false,
testing: {
testMode: true,
debugAudioLevels: true
@@ -228,7 +229,9 @@ describe('Start muted', () => {
},
p2p: {
enabled: true
- }
+ },
+ startWithAudioMuted: false,
+ startWithVideoMuted: false
}
});
diff --git a/tests/specs/misc/breakoutRooms.spec.ts b/tests/specs/misc/breakoutRooms.spec.ts
index f55480424f45..a68954d4f8b0 100644
--- a/tests/specs/misc/breakoutRooms.spec.ts
+++ b/tests/specs/misc/breakoutRooms.spec.ts
@@ -10,6 +10,7 @@ import {
} from '../../helpers/participants';
setTestProperties(__filename, {
+ retry: true,
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
diff --git a/tests/specs/misc/xmppConferenceRequest.spec.ts b/tests/specs/misc/xmppConferenceRequest.spec.ts
index db96f0905a8f..9ef42a3a07ec 100644
--- a/tests/specs/misc/xmppConferenceRequest.spec.ts
+++ b/tests/specs/misc/xmppConferenceRequest.spec.ts
@@ -10,6 +10,7 @@ describe('XMPP Conference Request', () => {
it('join with conferenceRequestUrl disabled', async () => {
await ensureOneParticipant({
skipWaitToJoin: true,
+ skipPrejoinButtonClick: true,
configOverwrite: {
prejoinConfig: {
enabled: true
diff --git a/tests/specs/moderation/lobby.spec.ts b/tests/specs/moderation/lobby.spec.ts
index f1df35592f15..c35bd52852e6 100644
--- a/tests/specs/moderation/lobby.spec.ts
+++ b/tests/specs/moderation/lobby.spec.ts
@@ -49,7 +49,7 @@ describe('Lobby', () => {
// media is being receiving and there are two remote streams
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
- await p3.waitForSendReceiveData();
+ await p3.waitForReceiveMedia();
await p3.waitForRemoteStreams(2);
// now check third one display name in the room, is the one set in the prejoin screen
@@ -107,7 +107,7 @@ describe('Lobby', () => {
// media is being receiving and there are two remote streams
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
- await p3.waitForSendReceiveData();
+ await p3.waitForReceiveMedia();
await p3.waitForRemoteStreams(2);
// now check third one display name in the room, is the one set in the prejoin screen
@@ -275,7 +275,7 @@ describe('Lobby', () => {
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
- await p3.waitForSendReceiveData();
+ await p3.waitForReceiveMedia();
});
it('enable with more than two participants', async () => {
@@ -430,19 +430,18 @@ async function enableLobby() {
async function enterLobby(participant: Participant, enterDisplayName = false, usePreJoin = false) {
const options: IJoinOptions = { };
- if (usePreJoin) {
- options.configOverwrite = {
- prejoinConfig: {
- enabled: true
- }
- };
- }
+ options.configOverwrite = {
+ prejoinConfig: {
+ enabled: usePreJoin
+ }
+ };
await ensureThreeParticipants({
...options,
skipDisplayName: true,
skipWaitToJoin: true,
- skipInMeetingChecks: true
+ skipInMeetingChecks: true,
+ skipPrejoinButtonClick: true
});
const { p3 } = ctx;
diff --git a/tests/specs/ui/preJoin.spec.ts b/tests/specs/ui/preJoin.spec.ts
index dfb1d55a0ab6..88492d74cc89 100644
--- a/tests/specs/ui/preJoin.spec.ts
+++ b/tests/specs/ui/preJoin.spec.ts
@@ -15,7 +15,8 @@ describe('Pre-join screen', () => {
requireDisplayName: true
},
skipDisplayName: true,
- skipWaitToJoin: true
+ skipWaitToJoin: true,
+ skipPrejoinButtonClick: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
@@ -42,7 +43,8 @@ describe('Pre-join screen', () => {
}
},
skipDisplayName: true,
- skipWaitToJoin: true
+ skipWaitToJoin: true,
+ skipPrejoinButtonClick: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
@@ -64,7 +66,8 @@ describe('Pre-join screen', () => {
}
},
skipDisplayName: true,
- skipWaitToJoin: true
+ skipWaitToJoin: true,
+ skipPrejoinButtonClick: true
});
const { p1 } = ctx;
@@ -111,7 +114,8 @@ describe('Pre-join screen', () => {
}
},
skipDisplayName: true,
- skipWaitToJoin: true
+ skipWaitToJoin: true,
+ skipPrejoinButtonClick: true
});
const p1PreJoinScreen = ctx.p2.getPreJoinScreen();
diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts
index 11e3528f433a..62862b763112 100644
--- a/tests/wdio.conf.ts
+++ b/tests/wdio.conf.ts
@@ -68,7 +68,7 @@ const specs = [
*/
function generateCapabilitiesFromSpecs(): Record {
const allSpecFiles: string[] = [];
- const browsers = [ 'p1', 'p2', 'p3', 'p4' ];
+ const browsers = [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ];
for (const pattern of specs) {
const matches = glob.sync(pattern, { cwd: path.join(__dirname) });
@@ -87,7 +87,9 @@ function generateCapabilitiesFromSpecs(): Record {
p1: new Set(),
p2: new Set(),
p3: new Set(),
- p4: new Set()
+ p4: new Set(),
+ p5: new Set(),
+ p6: new Set()
};
for (const file of allSpecFiles) {
@@ -194,6 +196,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
} ]
],
+ execArgv: [ '--stack-trace-limit=100' ],
+
// =====
// Hooks
// =====
@@ -286,9 +290,23 @@ export const config: WebdriverIO.MultiremoteConfig = {
keepAlive.forEach(clearInterval);
},
- beforeSession(c, capabilities_, specs_, cid) {
+ async beforeSession(c, capabilities_, spec, cid) {
const originalBefore = c.before;
+ if (spec && spec.length == 1) {
+ const testFilePath = spec[0].replace(/^file:\/\//, '');
+ const testProperties = await getTestProperties(testFilePath);
+
+ if (testProperties.retry) {
+ c.specFileRetries = 1;
+ c.specFileRetriesDeferred = true;
+ c.specFileRetriesDelay = 1;
+ console.log(`Enabling retry for ${testFilePath}`);
+ }
+ } else {
+ console.log('No test file or multiple test files specified, will not enable retries');
+ }
+
if (!originalBefore || !Array.isArray(originalBefore) || originalBefore.length !== 1) {
console.warn('No before hook found or more than one found, skipping');
diff --git a/yarn.lock b/yarn.lock
index b3b32f055029..158b64c8cbb4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4699,7 +4699,23 @@
resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-0.0.34.tgz#12b4a344274fb102ff2f6c877b37587bc3e46008"
integrity sha512-QSb9ojDincskc+uKMI0KXp8e1NALFINCrMlp8VGKGcTSxeEyRTTKyjWw75NYrCZHUsVEEEpr1tYHpbtaC++/sQ==
-"@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
+"@types/eslint-scope@^3.7.7":
+ version "3.7.7"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
+ integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*":
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584"
+ integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -4803,7 +4819,7 @@
resolved "https://registry.yarnpkg.com/@types/js-md5/-/js-md5-0.4.3.tgz#c134cbb71c75018876181f886a12a2f9962a312a"
integrity sha512-BIga/WEqTi35ccnGysOuO4RmwVnpajv9oDB/sDQSY2b7/Ac7RyYR30bv7otZwByMvOJV9Vqq6/O1DFAnOzE4Pg==
-"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
+"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -5652,7 +5668,7 @@
dependencies:
"@wdio/logger" "9.18.0"
-"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1":
+"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6"
integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==
@@ -5718,7 +5734,7 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1"
integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==
-"@webassemblyjs/wasm-edit@^1.12.1":
+"@webassemblyjs/wasm-edit@^1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597"
integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==
@@ -5753,7 +5769,7 @@
"@webassemblyjs/wasm-gen" "1.14.1"
"@webassemblyjs/wasm-parser" "1.14.1"
-"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1":
+"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb"
integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==
@@ -5838,10 +5854,10 @@ accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.7, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
-acorn-import-attributes@^1.9.5:
- version "1.9.5"
- resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
- integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
+acorn-import-phases@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
+ integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
acorn-jsx@^5.3.2:
version "5.3.2"
@@ -5855,7 +5871,7 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
-acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0, acorn@^8.7.1, acorn@^8.9.0:
+acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0, acorn@^8.9.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -6532,6 +6548,11 @@ baseline-browser-mapping@^2.8.25:
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz#5de72358cf363ac41e7d642af239f6ac5ed1270a"
integrity sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==
+baseline-browser-mapping@^2.9.0:
+ version "2.9.19"
+ resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488"
+ integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==
+
basic-ftp@^5.0.2:
version "5.0.5"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
@@ -6706,7 +6727,7 @@ browserify-sign@^4.2.3:
readable-stream "^2.3.8"
safe-buffer "^5.2.1"
-browserslist@^4.21.10, browserslist@^4.24.0, browserslist@^4.27.0, browserslist@^4.28.0:
+browserslist@^4.24.0, browserslist@^4.27.0, browserslist@^4.28.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929"
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
@@ -6717,6 +6738,17 @@ browserslist@^4.21.10, browserslist@^4.24.0, browserslist@^4.27.0, browserslist@
node-releases "^2.0.27"
update-browserslist-db "^1.1.4"
+browserslist@^4.28.1:
+ version "4.28.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
+ integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
+ dependencies:
+ baseline-browser-mapping "^2.9.0"
+ caniuse-lite "^1.0.30001759"
+ electron-to-chromium "^1.5.263"
+ node-releases "^2.0.27"
+ update-browserslist-db "^1.2.0"
+
bser@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
@@ -6857,6 +6889,11 @@ caniuse-lite@^1.0.30001754:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz#d569e7b010372c6b0ca3946e30dada0a2e9d5006"
integrity sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==
+caniuse-lite@^1.0.30001759:
+ version "1.0.30001769"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2"
+ integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==
+
chai@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.1.tgz#d1e64bc42433fbee6175ad5346799682060b5b6a"
@@ -8141,6 +8178,11 @@ electron-to-chromium@^1.5.249:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz#bec8f2887c30001dfacf415c136eae3b4386846a"
integrity sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==
+electron-to-chromium@^1.5.263:
+ version "1.5.286"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e"
+ integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==
+
elliptic@^6.5.3, elliptic@^6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06"
@@ -8207,7 +8249,7 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
-enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1:
+enhanced-resolve@^5.0.0:
version "5.18.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44"
integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==
@@ -8215,6 +8257,14 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1:
graceful-fs "^4.2.4"
tapable "^2.2.0"
+enhanced-resolve@^5.19.0:
+ version "5.19.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c"
+ integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.3.0"
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -8354,11 +8404,16 @@ es-iterator-helpers@^1.2.1:
iterator.prototype "^1.1.4"
safe-array-concat "^1.1.3"
-es-module-lexer@^1.2.1, es-module-lexer@^1.7.0:
+es-module-lexer@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
+es-module-lexer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1"
+ integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==
+
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
@@ -11081,9 +11136,9 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
-"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz":
- version "0.0.20-debug"
- resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz#3e5a6b196a215d2dc449c588c23ad32bded2b1de"
+"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.21/lib-jitsi-meet-0.0.21.tgz":
+ version "0.0.21"
+ resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.21/lib-jitsi-meet-0.0.21.tgz#8f5b3648e44c4d0b645810db20bd808d36c4d34b"
dependencies:
"@hexagon/base64" "^2.0.4"
"@jitsi/js-utils" "^2.6.7"
@@ -11148,7 +11203,7 @@ load-script@^1.0.0:
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
-loader-runner@^4.2.0:
+loader-runner@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
@@ -14248,7 +14303,7 @@ scheduler@^0.23.2:
dependencies:
loose-envify "^1.1.0"
-schema-utils@^3.0.0, schema-utils@^3.2.0:
+schema-utils@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
@@ -14257,7 +14312,7 @@ schema-utils@^3.0.0, schema-utils@^3.2.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
-schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0:
+schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46"
integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
@@ -15157,7 +15212,7 @@ tailwindcss@^4.1.17:
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.17.tgz#e6dcb7a9c60cef7522169b5f207ffec2fd652286"
integrity sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==
-tapable@^2.1.1, tapable@^2.2.0:
+tapable@^2.2.0, tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
@@ -15182,10 +15237,10 @@ tar-stream@^3.0.0, tar-stream@^3.1.5:
fast-fifo "^1.2.0"
streamx "^2.15.0"
-terser-webpack-plugin@^5.3.10:
- version "5.3.14"
- resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06"
- integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==
+terser-webpack-plugin@^5.3.16:
+ version "5.3.16"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
+ integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
@@ -15670,6 +15725,14 @@ update-browserslist-db@^1.1.4:
escalade "^3.2.0"
picocolors "^1.1.1"
+update-browserslist-db@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
+ integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -15875,10 +15938,10 @@ wasm-check@2.0.1:
resolved "https://registry.yarnpkg.com/wasm-check/-/wasm-check-2.0.1.tgz#de87b2fb876f42fc377898955773ba35a115b25e"
integrity sha512-5otny2JrfRNKIc+zi1YSOrNxXe47trEQbpY6g/MtHrFwLumKSJyAIobGXH1tlEBezE95eIsmDokBbUZtIZTvvA==
-watchpack@^2.4.1:
- version "2.4.4"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947"
- integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
+watchpack@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.1.tgz#dd38b601f669e0cbf567cb802e75cead82cde102"
+ integrity sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
@@ -16049,39 +16112,41 @@ webpack-merge@^5.7.3:
flat "^5.0.2"
wildcard "^2.0.0"
-webpack-sources@^3.2.3:
+webpack-sources@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
-webpack@5.95.0:
- version "5.95.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0"
- integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==
- dependencies:
- "@types/estree" "^1.0.5"
- "@webassemblyjs/ast" "^1.12.1"
- "@webassemblyjs/wasm-edit" "^1.12.1"
- "@webassemblyjs/wasm-parser" "^1.12.1"
- acorn "^8.7.1"
- acorn-import-attributes "^1.9.5"
- browserslist "^4.21.10"
+webpack@5.105.1:
+ version "5.105.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.1.tgz#c05cb3621196c76fa3b3a9bea446d14616b83778"
+ integrity sha512-Gdj3X74CLJJ8zy4URmK42W7wTZUJrqL+z8nyGEr4dTN0kb3nVs+ZvjbTOqRYPD7qX4tUmwyHL9Q9K6T1seW6Yw==
+ dependencies:
+ "@types/eslint-scope" "^3.7.7"
+ "@types/estree" "^1.0.8"
+ "@types/json-schema" "^7.0.15"
+ "@webassemblyjs/ast" "^1.14.1"
+ "@webassemblyjs/wasm-edit" "^1.14.1"
+ "@webassemblyjs/wasm-parser" "^1.14.1"
+ acorn "^8.15.0"
+ acorn-import-phases "^1.0.3"
+ browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
- enhanced-resolve "^5.17.1"
- es-module-lexer "^1.2.1"
+ enhanced-resolve "^5.19.0"
+ es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1"
- loader-runner "^4.2.0"
+ loader-runner "^4.3.1"
mime-types "^2.1.27"
neo-async "^2.6.2"
- schema-utils "^3.2.0"
- tapable "^2.1.1"
- terser-webpack-plugin "^5.3.10"
- watchpack "^2.4.1"
- webpack-sources "^3.2.3"
+ schema-utils "^4.3.3"
+ tapable "^2.3.0"
+ terser-webpack-plugin "^5.3.16"
+ watchpack "^2.5.1"
+ webpack-sources "^3.3.3"
webrtc-adapter@8.1.1:
version "8.1.1"