diff --git a/frontend/src/components/UnsupportedBrowser/UnsupportedBrowserMessage/UnsupportedBrowserMessage.tsx b/frontend/src/components/UnsupportedBrowser/UnsupportedBrowserMessage/UnsupportedBrowserMessage.tsx
index 921a31ce8..7dc33dcd7 100644
--- a/frontend/src/components/UnsupportedBrowser/UnsupportedBrowserMessage/UnsupportedBrowserMessage.tsx
+++ b/frontend/src/components/UnsupportedBrowser/UnsupportedBrowserMessage/UnsupportedBrowserMessage.tsx
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
const UnsupportedBrowserMessage = (): ReactElement => {
const { t } = useTranslation();
return (
-
+
{t('unsupportedBrowser.header')}
{t('unsupportedBrowser.message')}
diff --git a/frontend/src/components/VividIcon/VividIcon.tsx b/frontend/src/components/VividIcon/VividIcon.tsx
index c150f813f..4c867b0e9 100644
--- a/frontend/src/components/VividIcon/VividIcon.tsx
+++ b/frontend/src/components/VividIcon/VividIcon.tsx
@@ -1,6 +1,4 @@
-import React from 'react';
-
-interface VividIconProps {
+interface VividIconProps extends Record
{
name: string;
customSize: -6 | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5;
}
@@ -13,9 +11,9 @@ interface VividIconProps {
* @property {number} customSize - The size of the icon, ranging from -6 to 5. -6 is the smallest and 5 is the largest.
* @returns {React.ReactElement} The rendered VividIcon component.
*/
-const VividIcon: React.FC = ({ name, customSize }) => {
+const VividIcon = ({ name, customSize, ...props }: VividIconProps) => {
// @ts-expect-error custom element
- return ;
+ return ;
};
export default VividIcon;
diff --git a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx
index cbcc2ad60..f3de734ca 100644
--- a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx
+++ b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx
@@ -5,7 +5,9 @@ import useDevices from '@hooks/useDevices';
import { AllMediaDevices } from '@app-types/room';
import { allMediaDevices } from '@utils/mockData/device';
import { AppConfigProviderWrapperOptions, makeAppConfigProviderWrapper } from '@test/providers';
+import backgroundEffectsDialog$ from '@Context/BackgroundEffectsDialog';
import ControlPanel from '.';
+import composeProviders from '@utils/composeProviders';
vi.mock('@hooks/useDevices.tsx');
@@ -133,32 +135,6 @@ describe('ControlPanel', () => {
);
expect(screen.getByTestId('audioOutput-menu')).toBeVisible();
});
-
- it('is not rendered when allowDeviceSelection is false', () => {
- render(
- {}}
- handleVideoInputOpen={() => {}}
- handleAudioOutputOpen={() => {}}
- handleClose={() => {}}
- openAudioInput={false}
- openVideoInput={false}
- openAudioOutput={false}
- anchorEl={null}
- />,
- {
- appConfigOptions: {
- value: {
- waitingRoomSettings: {
- allowDeviceSelection: false,
- },
- },
- },
- }
- );
-
- expect(screen.queryByTestId('ControlPanel')).not.toBeInTheDocument();
- });
});
function render(
@@ -169,5 +145,7 @@ function render(
) {
const { AppConfigWrapper } = makeAppConfigProviderWrapper(options?.appConfigOptions);
- return renderBase(ui, { ...options, wrapper: AppConfigWrapper });
+ const wrapper = composeProviders(AppConfigWrapper, backgroundEffectsDialog$.Provider);
+
+ return renderBase(ui, { ...options, wrapper });
}
diff --git a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx
index 524536526..cdad9c0a9 100644
--- a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx
+++ b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx
@@ -1,16 +1,24 @@
-import { Button, SxProps } from '@mui/material';
-import { ReactElement, MouseEvent, TouchEvent } from 'react';
-import MicNone from '@mui/icons-material/MicNone';
-import VideoCall from '@mui/icons-material/VideoCall';
-import Speaker from '@mui/icons-material/Speaker';
-import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import { ReactElement, MouseEvent, TouchEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import usePreviewPublisherContext from '@hooks/usePreviewPublisherContext';
import useDevices from '@hooks/useDevices';
import useAudioOutputContext from '@hooks/useAudioOutputContext';
import useIsSmallViewport from '@hooks/useIsSmallViewport';
-import useAppConfig from '@Context/AppConfig/hooks/useAppConfig';
+import Box from '@ui/Box';
+import { SxProps } from '@ui/SxProps';
+import useTheme from '@ui/theme';
+import VividIcon from '@components/VividIcon';
+import ButtonBase from '@ui/ButtonBase';
import MenuDevicesWaitingRoom from '../MenuDevices';
+import MenuMoreOptions from '../MenuMoreOptions/MenuMoreOptions';
+
+const textSx: SxProps = {
+ flex: '1 1 0',
+ minWidth: 0,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+};
export type ControlPanelProps = {
handleAudioInputOpen: (
@@ -54,98 +62,136 @@ const ControlPanel = ({
openAudioOutput,
anchorEl,
}: ControlPanelProps): ReactElement | false => {
+ const [openMoreOptions, setOpenMoreOptions] = useState(false);
+ const [moreOptionsAnchorEl, setMoreOptionsAnchorEl] = useState(null);
+ const handleCloseMoreOptions = () => {
+ setOpenMoreOptions(false);
+ setMoreOptionsAnchorEl(null);
+ };
+ const handleOpenMoreOptions = (event: MouseEvent) => {
+ setMoreOptionsAnchorEl(event.currentTarget);
+ setOpenMoreOptions(true);
+ };
+
const { t } = useTranslation();
const isSmallViewport = useIsSmallViewport();
const { allMediaDevices } = useDevices();
const { localAudioSource, localVideoSource, changeAudioSource, changeVideoSource } =
usePreviewPublisherContext();
const { currentAudioOutputDevice, setAudioOutputDevice } = useAudioOutputContext();
-
- const allowDeviceSelection = useAppConfig(
- ({ waitingRoomSettings }) => waitingRoomSettings.allowDeviceSelection
- );
+ const theme = useTheme();
const buttonSx: SxProps = {
- borderRadius: '10px',
- color: 'rgb(95, 99, 104)',
- textTransform: 'none', // ensures that the text is not upper case
- border: 'none',
- boxShadow: 'none',
- whiteSpace: 'nowrap',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1.5,
'&:hover': {
- border: 'none',
- boxShadow: 'none',
+ backgroundColor: theme.colors.background,
},
};
return (
- allowDeviceSelection && (
-
-
- }
- variant="outlined"
- startIcon={}
- aria-controls={openVideoInput ? 'basic-menu' : undefined}
- aria-haspopup="true"
- aria-expanded={openVideoInput ? 'true' : undefined}
- onClick={handleAudioInputOpen}
- >
+
+
+
+
+
{isSmallViewport
? t('devices.audio.microphone.short')
: t('devices.audio.microphone.full')}
-
-
- }
- sx={buttonSx}
- variant="outlined"
- startIcon={}
- aria-label={t('devices.video.camera.button.ariaLabel')}
- >
+
+
+
+
+
+
+
+
{t('button.camera')}
-
+
+
+
+
-
- }
- sx={buttonSx}
- variant="outlined"
- startIcon={}
- >
+
+
+
{t('button.speaker')}
-
-
-
-
- )
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.spec.tsx b/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.spec.tsx
index 6529c4fe4..c5232bc1a 100644
--- a/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.spec.tsx
+++ b/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.spec.tsx
@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react';
import { describe, beforeEach, it, Mock, vi, expect } from 'vitest';
import MenuDevices from './MenuDevices';
-import * as util from '../../../utils/util';
+import * as util from '@utils/util';
-vi.mock('../../../utils/util', async () => {
- const actual = await vi.importActual('../../../utils/util');
+vi.mock('@utils/util', async () => {
+ const actual = await vi.importActual('@utils/util');
return {
...actual,
isGetActiveAudioOutputDeviceSupported: vi.fn(),
diff --git a/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.tsx b/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.tsx
index b4529c9a4..1ab9048dc 100644
--- a/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.tsx
+++ b/frontend/src/components/WaitingRoom/MenuDevices/MenuDevices.tsx
@@ -1,10 +1,12 @@
-import { Menu, MenuItem } from '@mui/material';
-import { Speaker } from '@mui/icons-material';
import { AudioOutputDevice, Device } from '@vonage/client-sdk-video';
import { ReactElement, useMemo } from 'react';
-import { isGetActiveAudioOutputDeviceSupported } from '../../../utils/util';
+import MenuItem from '@ui/MenuItem';
+import Menu from '@ui/Menu';
+import VividIcon from '@components/VividIcon';
+import Box from '@ui/Box';
+import cleanAndDedupeDeviceLabels from '@utils/cleanAndDedupeDeviceLabels/cleanAndDedupeDeviceLabels';
import SoundTest from '../../SoundTest';
-import cleanAndDedupeDeviceLabels from '../../../utils/cleanAndDedupeDeviceLabels/cleanAndDedupeDeviceLabels';
+import { isGetActiveAudioOutputDeviceSupported } from '@utils/util';
export type MenuDevicesWaitingRoomProps = {
onClose: () => void;
@@ -67,24 +69,15 @@ const MenuDevices = ({
}}
key={device.deviceId}
selected={device.deviceId === localSource}
- sx={{
- pl: 4,
- backgroundColor: device.deviceId === localSource ? 'rgba(26,115,232,.9)' : '',
- }}
>
{device.label}
))}
{deviceType === 'audioOutput' && (
-
+
+
+
)}
diff --git a/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.spec.tsx b/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.spec.tsx
new file mode 100644
index 000000000..6eea39d4a
--- /dev/null
+++ b/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.spec.tsx
@@ -0,0 +1,53 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render as renderBase, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ReactElement } from 'react';
+import backgroundEffectsDialog$ from '@Context/BackgroundEffectsDialog';
+import MenuMoreOptions from './MenuMoreOptions';
+
+describe('MenuMoreOptions', () => {
+ const mockOnClose = vi.fn();
+ const mockAnchorEl = document.createElement('button');
+
+ beforeEach(() => {
+ mockOnClose.mockClear();
+ });
+
+ it('should render when open is true', () => {
+ render();
+
+ expect(screen.getByTestId('menu-more-options')).toBeInTheDocument();
+ });
+
+ it('should not render menu items when open is false', () => {
+ render();
+
+ expect(screen.queryByText(/background effects/i)).not.toBeInTheDocument();
+ });
+
+ it('should display background effects option', () => {
+ render();
+
+ expect(screen.getByText(/background effects/i)).toBeInTheDocument();
+ });
+
+ it('should call onClose when clicking on background effects option', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const menuItem = screen.getByText(/background effects/i);
+ await user.click(menuItem);
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('should display icon for background effects', () => {
+ render();
+
+ expect(screen.getByTestId('vivid-icon-gallery-line')).toBeInTheDocument();
+ });
+});
+
+function render(ui: ReactElement) {
+ return renderBase(ui, { wrapper: backgroundEffectsDialog$.Provider });
+}
diff --git a/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.tsx b/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.tsx
new file mode 100644
index 000000000..6137c746a
--- /dev/null
+++ b/frontend/src/components/WaitingRoom/MenuMoreOptions/MenuMoreOptions.tsx
@@ -0,0 +1,60 @@
+import { ReactElement, useCallback } from 'react';
+import MenuItem from '@ui/MenuItem';
+import Menu from '@ui/Menu';
+import { useTranslation } from 'react-i18next';
+import VividIcon from '@components/VividIcon';
+import Box from '@ui/Box';
+import backgroundEffectsDialog$ from '@Context/BackgroundEffectsDialog';
+
+export type MenuMoreOptionsWaitingRoomProps = {
+ onClose: () => void;
+ open: boolean;
+ anchorEl: HTMLElement | null;
+};
+
+/**
+ * MenuMoreOptions Component
+ *
+ * Displays a list of options in the waiting room.
+ * @param {MenuMoreOptionsWaitingRoomProps} props - The props for the component.
+ * @property {Function} onClose - Menu close handler.
+ * @property {boolean} open - Whether the menu is open or not.
+ * @property {HTMLElement | null} anchorEl - The anchor element.
+ * @returns {ReactElement} - The MenuMoreOptions component
+ */
+const MenuMoreOptions = ({
+ onClose,
+ open,
+ anchorEl,
+}: MenuMoreOptionsWaitingRoomProps): ReactElement => {
+ const { t } = useTranslation();
+ const { open: openBackgroundEffects } = backgroundEffectsDialog$.use.actions();
+
+ const handleClick = useCallback(() => {
+ openBackgroundEffects();
+ onClose();
+ }, [openBackgroundEffects, onClose]);
+
+ return (
+
+ );
+};
+
+export default MenuMoreOptions;
diff --git a/frontend/src/components/WaitingRoom/MenuMoreOptions/index.tsx b/frontend/src/components/WaitingRoom/MenuMoreOptions/index.tsx
new file mode 100644
index 000000000..aae4ca14e
--- /dev/null
+++ b/frontend/src/components/WaitingRoom/MenuMoreOptions/index.tsx
@@ -0,0 +1,3 @@
+import MenuMoreOptions from './MenuMoreOptions';
+
+export default MenuMoreOptions;
diff --git a/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx b/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx
index 4e77e905c..5c400eb20 100644
--- a/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx
+++ b/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx
@@ -1,13 +1,18 @@
-import { TextField, Button, InputAdornment } from '@mui/material';
import React, { Dispatch, MouseEvent, ReactElement, SetStateAction, useState } from 'react';
-import { PersonOutline } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import useUserContext from '../../../hooks/useUserContext';
-import { UserType } from '../../../Context/user';
-import useRoomName from '../../../hooks/useRoomName';
-import isValidRoomName from '../../../utils/isValidRoomName';
-import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage';
+import TextField from '@ui/TextField';
+import Button from '@ui/Button';
+import Box from '@ui/Box';
+import Typography from '@ui/Typography';
+import Card from '@ui/Card';
+import useTheme from '@ui/theme';
+import useUserContext from '@hooks/useUserContext';
+import { UserType } from '@Context/user';
+import useRoomName from '@hooks/useRoomName';
+import isValidRoomName from '@utils/isValidRoomName';
+import isValidUserName from '@utils/isValidUserName';
+import { setStorageItem, STORAGE_KEYS } from '@utils/storage';
export type UserNameInputProps = {
username: string;
@@ -29,20 +34,16 @@ const UsernameInput = ({ username, setUsername }: UserNameInputProps): ReactElem
const navigate = useNavigate();
const roomName = useRoomName();
const [isUserNameInvalid, setIsUserNameInvalid] = useState(false);
+ const theme = useTheme();
const onChangeParticipantName = (e: React.ChangeEvent) => {
const inputUserName = e.target.value;
- if (inputUserName === '' || inputUserName.trim() === '') {
- // Space detected
- setUsername('');
- return;
- }
setIsUserNameInvalid(false);
setUsername(inputUserName);
};
const validateForm = () => {
- if (username === '') {
+ if (!isValidUserName(username)) {
setIsUserNameInvalid(true);
return false;
}
@@ -75,62 +76,43 @@ const UsernameInput = ({ username, setUsername }: UserNameInputProps): ReactElem
};
return (
-
+
+ {t('waitingRoom.user.input.title')}
+
+
+
+
+ {t('waitingRoom.title')}
+
+
+
+ {roomName}
+
+
+
+
);
};
diff --git a/frontend/src/components/WaitingRoom/VideoContainer/VideoContainer.tsx b/frontend/src/components/WaitingRoom/VideoContainer/VideoContainer.tsx
index 29597ab0b..dc7a814bd 100644
--- a/frontend/src/components/WaitingRoom/VideoContainer/VideoContainer.tsx
+++ b/frontend/src/components/WaitingRoom/VideoContainer/VideoContainer.tsx
@@ -13,6 +13,7 @@ import VignetteEffect from '../VignetteEffect';
import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
import BackgroundEffectsDialog from '../BackgroundEffects/BackgroundEffectsDialog';
import BackgroundEffectsButton from '../BackgroundEffects/BackgroundEffectsButton';
+import backgroundEffectsDialog$ from '@Context/BackgroundEffectsDialog';
export type VideoContainerProps = {
username: string;
@@ -30,7 +31,7 @@ export type VideoContainerProps = {
const VideoContainer = ({ username }: VideoContainerProps): ReactElement => {
const containerRef = useRef(null);
const [isVideoLoading, setIsVideoLoading] = useState(true);
- const [isBackgroundEffectsOpen, setIsBackgroundEffectsOpen] = useState(false);
+ const [{ isOpen: isBackgroundEffectsOpen }, { open, close }] = backgroundEffectsDialog$.use();
const { user } = useUserContext();
const { publisherVideoElement, isVideoEnabled, isAudioEnabled, speechLevel } =
usePreviewPublisherContext();
@@ -94,11 +95,13 @@ const VideoContainer = ({ username }: VideoContainerProps): ReactElement => {
- setIsBackgroundEffectsOpen(true)} />
-
+
+ {isBackgroundEffectsOpen && (
+
+ )}
)}
diff --git a/frontend/src/designTokens/index.ts b/frontend/src/designTokens/index.ts
deleted file mode 100644
index 4fc6d19ea..000000000
--- a/frontend/src/designTokens/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import designTokens from './designTokens.ts';
-
-export default designTokens;
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 1a08fbad6..410ceab1e 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -47,7 +47,7 @@
"button.startNewRoom": "Start a new video meeting",
"button.joinExistingMeeting": "Join existing meeting",
"button.joinWaitingRoom": "Join waiting room",
- "button.join": "Join",
+ "button.join": "Join meeting",
"button.mute": "Mute",
"button.send": "Send",
"button.speaker": "Speaker",
@@ -89,7 +89,6 @@
"devices.video.blur.ariaLabel": "Toggle background blur",
"devices.video.blur.label": "Blur your background",
"devices.video.camera.ariaLabel": "toggle video",
- "devices.video.camera.button.ariaLabel": "video",
"devices.video.camera.state.on": "Enable camera",
"devices.video.camera.state.off": "Disable camera",
"devices.video.camera.full": "Camera",
@@ -192,9 +191,10 @@
"unknown.device": "Unknown Device",
"user.unknown": "unknown user",
"user.you": "You",
- "waitingRoom.title": "Prepare to join:",
- "waitingRoom.user.input.label": "What is your name?",
- "waitingRoom.user.input.placeholder": "Enter your name",
+ "waitingRoom.title": "Prepare to join",
+ "waitingRoom.user.input.title": "Enter your name",
+ "waitingRoom.user.input.label": "Name",
+ "waitingRoom.user.input.error": "Name cannot be empty or contain special characters.",
"zoom.reset": "Reset Zoom",
"zoom.start": "Start Zooming",
"zoom.out": "Zoom out",
diff --git a/frontend/src/locales/es-MX.json b/frontend/src/locales/es-MX.json
index 66f1fbac8..1e0d1e89e 100644
--- a/frontend/src/locales/es-MX.json
+++ b/frontend/src/locales/es-MX.json
@@ -47,7 +47,7 @@
"button.startNewRoom": "Iniciar una nueva videoconferencia",
"button.joinExistingMeeting": "Unirse a una reunión existente",
"button.joinWaitingRoom": "Unirse a la sala de espera",
- "button.join": "Unirse",
+ "button.join": "Unirse a la reunión",
"button.mute": "Silenciar",
"button.send": "Enviar",
"button.speaker": "Altavoz",
@@ -88,7 +88,6 @@
"devices.video.blur.ariaLabel": "Activar/desactivar desenfoque de fondo",
"devices.video.blur.label": "Desenfocar fondo",
"devices.video.camera.ariaLabel": "activar/desactivar video",
- "devices.video.camera.button.ariaLabel": "video",
"devices.video.camera.state.on": "Encender cámara",
"devices.video.camera.state.off": "Apagar cámara",
"devices.video.camera.full": "Cámara",
@@ -190,9 +189,10 @@
"unknown.device": "Dispositivo desconocido",
"user.unknown": "usuario desconocido",
"user.you": "Tú",
- "waitingRoom.title": "Prepárate para unirte:",
- "waitingRoom.user.input.label": "¿Cuál es tu nombre?",
- "waitingRoom.user.input.placeholder": "Escribe tu nombre",
+ "waitingRoom.title": "Prepárate para unirte",
+ "waitingRoom.user.input.title": "Escribe tu nombre",
+ "waitingRoom.user.input.label": "Nombre",
+ "waitingRoom.user.input.error": "El nombre no puede estar vacío ni contener caracteres especiales.",
"zoom.reset": "Restablecer zoom",
"zoom.start": "Iniciar zoom",
"zoom.out": "Alejar zoom",
diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json
index d87618eb2..13ca9e3eb 100644
--- a/frontend/src/locales/es.json
+++ b/frontend/src/locales/es.json
@@ -47,7 +47,7 @@
"button.startNewRoom": "Iniciar una nueva videoconferencia",
"button.joinExistingMeeting": "Unirse a una reunión existente",
"button.joinWaitingRoom": "Unirse a la sala de espera",
- "button.join": "Unirse",
+ "button.join": "Unirse a la reunión",
"button.mute": "Silenciar",
"button.send": "Enviar",
"button.speaker": "Altavoz",
@@ -88,7 +88,6 @@
"devices.video.blur.ariaLabel": "Alternar desenfoque de fondo",
"devices.video.blur.label": "Desenfoca tu fondo",
"devices.video.camera.ariaLabel": "alternar video",
- "devices.video.camera.button.ariaLabel": "video",
"devices.video.camera.state.on": "Activar cámara",
"devices.video.camera.state.off": "Desactivar cámara",
"devices.video.camera.full": "Cámara",
@@ -190,9 +189,10 @@
"unknown.device": "Dispositivo desconocido",
"user.unknown": "usuario desconocido",
"user.you": "Tú",
- "waitingRoom.title": "Prepararse para unirse:",
- "waitingRoom.user.input.label": "¿Cuál es tu nombre?",
- "waitingRoom.user.input.placeholder": "Ingresa tu nombre",
+ "waitingRoom.title": "Prepararse para unirse",
+ "waitingRoom.user.input.title": "Ingresa tu nombre",
+ "waitingRoom.user.input.label": "Nombre",
+ "waitingRoom.user.input.error": "El nombre no puede estar vacío ni contener caracteres especiales.",
"zoom.reset": "Restablecer zoom",
"zoom.start": "Iniciar zoom",
"zoom.out": "Alejar zoom",
diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json
index 3aefe2f0b..d8f538acb 100644
--- a/frontend/src/locales/it.json
+++ b/frontend/src/locales/it.json
@@ -47,7 +47,7 @@
"button.startNewRoom": "Inizia una nuova videoconferenza",
"button.joinExistingMeeting": "Unisciti a una riunione esistente",
"button.joinWaitingRoom": "Unisciti alla sala d'attesa",
- "button.join": "Partecipa",
+ "button.join": "Partecipa alla riunione",
"button.mute": "Disattiva audio",
"button.send": "Invia",
"button.speaker": "Altoparlante",
@@ -88,7 +88,6 @@
"devices.video.blur.ariaLabel": "Attiva la sfocatura dello sfondo",
"devices.video.blur.label": "Sfoca lo sfondo",
"devices.video.camera.ariaLabel": "attiva/disattiva video",
- "devices.video.camera.button.ariaLabel": "video",
"devices.video.camera.state.on": "Attiva camera",
"devices.video.camera.state.off": "Disattiva camera",
"devices.video.camera.full": "Camera",
@@ -190,9 +189,10 @@
"unknown.device": "Dispositivo sconosciuto",
"user.unknown": "utente sconosciuto",
"user.you": "Tu",
- "waitingRoom.title": "Preparati per unirti:",
- "waitingRoom.user.input.label": "Qual è il tuo nome?",
- "waitingRoom.user.input.placeholder": "Inserisci il tuo nome",
+ "waitingRoom.title": "Preparati per unirti",
+ "waitingRoom.user.input.title": "Inserisci il tuo nome",
+ "waitingRoom.user.input.label": "Nome",
+ "waitingRoom.user.input.error": "Il nome non può essere vuoto né contenere caratteri speciali.",
"zoom.reset": "Reimposta zoom",
"zoom.start": "Inizia zoom",
"zoom.out": "Zoom indietro",
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index c0b014566..77594cb80 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,9 +1,7 @@
import ReactDOM from 'react-dom/client';
import { registerIcon } from '@vonage/vivid';
-import Box from '@ui/Box';
import App from './App.jsx';
import './i18n.js';
-import designTokens from './designTokens/designTokens.js';
// Register Vivid icons for use throughout the application
registerIcon();
@@ -14,18 +12,4 @@ registerIcon();
*/
const rootElement = document.getElementById('root')!;
-ReactDOM.createRoot(rootElement).render(
-
-
-
-);
+ReactDOM.createRoot(rootElement).render(
);
diff --git a/frontend/src/pages/GoodBye/GoodBye.tsx b/frontend/src/pages/GoodBye/GoodBye.tsx
index f5d7314d6..b0bf601db 100644
--- a/frontend/src/pages/GoodBye/GoodBye.tsx
+++ b/frontend/src/pages/GoodBye/GoodBye.tsx
@@ -1,7 +1,7 @@
import { useLocation } from 'react-router-dom';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
-import FlexLayout from '@ui/FlexLayout';
+import PageLayout from '@ui/PageLayout';
import Banner from '@components/Banner';
import Footer from '@components/Footer/Footer';
import useArchives from '../../hooks/useArchives';
@@ -29,23 +29,23 @@ const GoodBye = (): ReactElement => {
const caption: string = location.state?.caption || t('goodbye.default.message');
return (
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
);
};
diff --git a/frontend/src/pages/LandingPage/LandingPage.tsx b/frontend/src/pages/LandingPage/LandingPage.tsx
index 05bc2f032..ab5ad5f06 100644
--- a/frontend/src/pages/LandingPage/LandingPage.tsx
+++ b/frontend/src/pages/LandingPage/LandingPage.tsx
@@ -1,5 +1,5 @@
import { ReactElement } from 'react';
-import FlexLayout from '@ui/FlexLayout';
+import PageLayout from '@ui/PageLayout';
import Banner from '@components/Banner';
import Footer from '@components/Footer/Footer';
import LandingPageWelcome from '../../components/LandingPageWelcome';
@@ -18,20 +18,20 @@ import RoomJoinContainer from '../../components/RoomJoinContainer';
*/
const LandingPage = (): ReactElement => {
return (
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
);
};
diff --git a/frontend/src/pages/UnsupportedBrowserPage/UnsupportedBrowserPage.tsx b/frontend/src/pages/UnsupportedBrowserPage/UnsupportedBrowserPage.tsx
index eb1926ec0..3c39bbf64 100644
--- a/frontend/src/pages/UnsupportedBrowserPage/UnsupportedBrowserPage.tsx
+++ b/frontend/src/pages/UnsupportedBrowserPage/UnsupportedBrowserPage.tsx
@@ -1,5 +1,5 @@
import { ReactElement } from 'react';
-import FlexLayout from '@ui/FlexLayout';
+import PageLayout from '@ui/PageLayout';
import Banner from '@components/Banner';
import Footer from '@components/Footer/Footer';
import SupportedBrowsers from '../../components/UnsupportedBrowser/SupportedBrowsers';
@@ -16,20 +16,20 @@ import UnsupportedBrowserMessage from '../../components/UnsupportedBrowser/Unsup
*/
const UnsupportedBrowserPage = (): ReactElement => {
return (
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
);
};
diff --git a/frontend/src/pages/WaitingRoom/WaitingRoom.spec.tsx b/frontend/src/pages/WaitingRoom/WaitingRoom.spec.tsx
index 57eed33b3..23fa2801e 100644
--- a/frontend/src/pages/WaitingRoom/WaitingRoom.spec.tsx
+++ b/frontend/src/pages/WaitingRoom/WaitingRoom.spec.tsx
@@ -171,7 +171,7 @@ describe('WaitingRoom', () => {
expect(screen.getByText('test-room-name')).toBeInTheDocument();
// Submit a name to navigate away from the waiting room
- const input = screen.getByPlaceholderText('Enter your name');
+ const input = screen.getByRole('textbox', { name: /name/i });
await user.type(input, 'Betsey Trotwood');
expect(input).toHaveValue('Betsey Trotwood');
@@ -194,6 +194,38 @@ describe('WaitingRoom', () => {
rerender(
);
expect(globalThis.location.reload).toBeCalled();
});
+
+ it('should not render ControlPanel when allowDeviceSelection is false', () => {
+ previewPublisherContext.accessStatus = DEVICE_ACCESS_STATUS.ACCEPTED;
+
+ const { queryByTestId } = render(
, {
+ appConfigOptions: {
+ value: {
+ waitingRoomSettings: {
+ allowDeviceSelection: false,
+ },
+ },
+ },
+ });
+
+ expect(queryByTestId('ControlPanel')).not.toBeInTheDocument();
+ });
+
+ it('should render ControlPanel when allowDeviceSelection is true', () => {
+ previewPublisherContext.accessStatus = DEVICE_ACCESS_STATUS.ACCEPTED;
+
+ const { queryByTestId } = render(
, {
+ appConfigOptions: {
+ value: {
+ waitingRoomSettings: {
+ allowDeviceSelection: true,
+ },
+ },
+ },
+ });
+
+ expect(queryByTestId('ControlPanel')).toBeInTheDocument();
+ });
});
/**
diff --git a/frontend/src/pages/WaitingRoom/WaitingRoom.tsx b/frontend/src/pages/WaitingRoom/WaitingRoom.tsx
index 9f0d08e11..e8c9d7085 100644
--- a/frontend/src/pages/WaitingRoom/WaitingRoom.tsx
+++ b/frontend/src/pages/WaitingRoom/WaitingRoom.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, MouseEvent, ReactElement, TouchEvent } from 'react';
import Box from '@ui/Box';
-import FlexLayout from '@ui/FlexLayout';
+import PageLayout from '@ui/PageLayout';
import Banner from '@components/Banner';
import Footer from '@components/Footer/Footer';
import usePreviewPublisherContext from '../../hooks/usePreviewPublisherContext';
@@ -12,6 +12,9 @@ import DeviceAccessAlert from '../../components/DeviceAccessAlert';
import { getStorageItem, STORAGE_KEYS } from '../../utils/storage';
import useIsSmallViewport from '../../hooks/useIsSmallViewport';
import useBackgroundPublisherContext from '../../hooks/useBackgroundPublisherContext';
+import backgroundEffectsDialog$ from '../../Context/BackgroundEffectsDialog';
+import classNames from 'classnames';
+import useAppConfig from '@Context/AppConfig/hooks/useAppConfig';
/**
* WaitingRoom Component
@@ -41,6 +44,10 @@ const WaitingRoom = (): ReactElement => {
const [username, setUsername] = useState(getStorageItem(STORAGE_KEYS.USERNAME) ?? '');
const isSmallViewport = useIsSmallViewport();
+ const allowDeviceSelection = useAppConfig(
+ ({ waitingRoomSettings }) => waitingRoomSettings.allowDeviceSelection
+ );
+
useEffect(() => {
if (!publisher) {
initLocalPublisher();
@@ -96,41 +103,46 @@ const WaitingRoom = (): ReactElement => {
};
return (
-
-
-
-
-
-
-
-
- {accessStatus === DEVICE_ACCESS_STATUS.ACCEPTED && (
-
- )}
-
-
-
-
-
-
-
-
-
- {accessStatus !== DEVICE_ACCESS_STATUS.ACCEPTED && (
-
- )}
-
+
+
+
+
+
+
+
+
+
+
+ {allowDeviceSelection && accessStatus === DEVICE_ACCESS_STATUS.ACCEPTED && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {accessStatus !== DEVICE_ACCESS_STATUS.ACCEPTED && (
+
+ )}
+
+
);
};
diff --git a/frontend/src/utils/isValidUserName/index.ts b/frontend/src/utils/isValidUserName/index.ts
new file mode 100644
index 000000000..75f861bcf
--- /dev/null
+++ b/frontend/src/utils/isValidUserName/index.ts
@@ -0,0 +1 @@
+export { default } from './isValidUserName';
diff --git a/frontend/src/utils/isValidUserName/isValidUserName.spec.ts b/frontend/src/utils/isValidUserName/isValidUserName.spec.ts
new file mode 100644
index 000000000..d06ab4314
--- /dev/null
+++ b/frontend/src/utils/isValidUserName/isValidUserName.spec.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect } from 'vitest';
+import isValidUserName from './isValidUserName';
+
+describe('isValidUserName', () => {
+ describe('valid usernames', () => {
+ const validCases = [
+ { input: 'John Doe', description: 'name with space' },
+ { input: 'María García', description: 'name with accents' },
+ { input: "O'Brien", description: 'name with apostrophe' },
+ { input: 'Jean-Pierre', description: 'name with hyphen' },
+ { input: 'User_123', description: 'name with underscore and numbers' },
+ { input: 'John.Smith', description: 'name with period' },
+ { input: 'A', description: 'single character' },
+ { input: 'User', description: 'simple name' },
+ { input: 'José Luis', description: 'name with special character' },
+ ];
+
+ validCases.forEach(({ input, description }) => {
+ it(`should return true for ${description}: "${input}"`, () => {
+ expect(isValidUserName(input)).toBe(true);
+ });
+ });
+ });
+
+ describe('invalid usernames', () => {
+ const invalidCases = [
+ { input: '', description: 'empty string' },
+ { input: ' ', description: 'only whitespace' },
+ { input: ' John', description: 'leading space' },
+ { input: 'John ', description: 'trailing space' },
+ { input: 'John Doe', description: 'multiple consecutive spaces' },
+ { input: 'John@Doe', description: 'special character @' },
+ { input: 'John#Doe', description: 'special character #' },
+ { input: 'John$Doe', description: 'special character $' },
+ { input: 'John