Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ This application provides features for common conferencing use cases, such as:
<img src="docs/assets/NoiseSupression.png" alt="Screenshot of noise supression toggle">
</details>
- <details>
<summary>Background effects in meeting and waiting room. You can set predefined images, custom image or slight/strong background blur. Images can be uploaded from local device or URL in these formats: JPG, PNG, GIF or BMP.</summary>
<summary>Background effects in meeting and waiting room. You can set predefined images, custom image or slight/strong background blur. Images can be uploaded from local device or URL in these formats: JPG, PNG, GIF or BMP. Background effects are not supported in non-Chromium-based browsers or on iOS. </summary>
<img src="docs/assets/BGEffects.png" alt="Screenshot of background effects">
</details>
- <details>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeAll } from 'vitest';
import AddBackgroundEffectLayout from './AddBackgroundEffectLayout';
import enTranslations from '../../../../locales/en.json';

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, string | number>) => {
const translations: Record<string, string> = {
'backgroundEffects.invalidFileType': enTranslations['backgroundEffects.invalidFileType'],
'backgroundEffects.fileTooLarge': enTranslations['backgroundEffects.fileTooLarge'],
'backgroundEffects.linkPlaceholder': enTranslations['backgroundEffects.linkPlaceholder'],
'backgroundEffects.dragDropText': enTranslations['backgroundEffects.dragDropText'],
'backgroundEffects.maxSize': enTranslations['backgroundEffects.maxSize'],
};

let translation = translations[key] || key;

if (options && typeof translation === 'string') {
Object.keys(options).forEach((param) => {
translation = translation.replace(`{{${param}}}`, String(options[param]));
});
}

return translation;
},
}),
}));

vi.mock('../../../../utils/useImageStorage/useImageStorage', () => ({
__esModule: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { ChangeEvent, ReactElement, useState } from 'react';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import LinkIcon from '@mui/icons-material/Link';
import { useTranslation } from 'react-i18next';
import FileUploader from '../../FileUploader/FileUploader';
import { ALLOWED_TYPES, MAX_SIZE_MB } from '../../../../utils/constants';
import useImageStorage from '../../../../utils/useImageStorage/useImageStorage';
Expand All @@ -32,6 +33,7 @@ const AddBackgroundEffectLayout = ({
const [imageLink, setImageLink] = useState<string>('');
const [linkLoading, setLinkLoading] = useState<boolean>(false);
const { storageError, handleImageFromFile, handleImageFromLink } = useImageStorage();
const { t } = useTranslation();

type HandleFileChangeType = ChangeEvent<HTMLInputElement> | { target: { files: FileList } };

Expand All @@ -47,12 +49,12 @@ const AddBackgroundEffectLayout = ({
}

if (!ALLOWED_TYPES.includes(file.type)) {
setFileError('Only JPG, PNG, GIF, or BMP images are allowed.');
setFileError(t('backgroundEffects.invalidFileType'));
return;
}

if (file.size > MAX_SIZE_MB * 1024 * 1024) {
setFileError(`Image must be less than ${MAX_SIZE_MB}MB.`);
setFileError(t('backgroundEffects.fileTooLarge', { maxSize: MAX_SIZE_MB }));
return;
}

Expand All @@ -63,7 +65,7 @@ const AddBackgroundEffectLayout = ({
customBackgroundImageChange(newImage.dataUrl);
}
} catch {
setFileError('Failed to process uploaded image.');
setFileError(t('backgroundEffects.processingError'));
}
};

Expand All @@ -76,7 +78,7 @@ const AddBackgroundEffectLayout = ({
setFileError('');
customBackgroundImageChange(newImage.dataUrl);
} else {
setFileError('Failed to store image.');
setFileError(t('backgroundEffects.storageError'));
}
} catch {
// error handled in hook
Expand All @@ -103,7 +105,7 @@ const AddBackgroundEffectLayout = ({
<TextField
fullWidth
size="small"
placeholder="Link from the web"
placeholder={t('backgroundEffects.linkPlaceholder')}
className="add-background-effect-input"
value={imageLink}
onChange={(e) => setImageLink(e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, Tabs, Tab } from '@mui/material';
import { Publisher } from '@vonage/client-sdk-video';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import EffectOptionButtons from '../EffectOptionButtons/EffectOptionButtons';
import BackgroundGallery from '../BackgroundGallery/BackgroundGallery';
import AddBackgroundEffectLayout from '../AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout';
Expand Down Expand Up @@ -58,6 +59,7 @@ const BackgroundEffectTabs = ({
setBackgroundSelected(value);
};
const isTabletViewport = useIsTabletViewport();
const { t } = useTranslation();

return (
<Box
Expand All @@ -81,10 +83,10 @@ const BackgroundEffectTabs = ({
}}
value={tabSelected}
onChange={(_event, newValue) => setTabSelected(newValue)}
aria-label="backgrounds tabs"
aria-label={t('backgroundEffects.title.tabs')}
>
<Tab sx={{ textTransform: 'none' }} label="Backgrounds" />
<Tab sx={{ textTransform: 'none' }} label="Add Background" />
<Tab sx={{ textTransform: 'none' }} label={t('backgroundEffects.tabs.backgrounds')} />
<Tab sx={{ textTransform: 'none' }} label={t('backgroundEffects.tabs.addBackground')} />
</Tabs>

<Box className="choose-background-effect-box" flex={1} minWidth={0}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ vi.mock('react-i18next', () => ({
const translations: Record<string, string> = {
'backgroundEffects.title': enTranslations['backgroundEffects.title'],
'backgroundEffects.choice': enTranslations['backgroundEffects.choice'],
'backgroundEffects.tabs.backgrounds': enTranslations['backgroundEffects.tabs.backgrounds'],
'backgroundEffects.tabs.addBackground':
enTranslations['backgroundEffects.tabs.addBackground'],
'button.cancel': enTranslations['button.cancel'],
'button.apply': enTranslations['button.apply'],
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BackgroundGallery, { backgrounds } from './BackgroundGallery';
import BackgroundGallery from './BackgroundGallery';
import enTranslations from '../../../locales/en.json';

const customImages = [
{ id: 'custom1', dataUrl: 'data:image/png;base64,custom1' },
{ id: 'custom2', dataUrl: 'data:image/png;base64,custom2' },
];

const backgrounds = [
{
id: 'bg4',
file: 'hogwarts.jpg',
name: enTranslations['backgroundEffects.backgrounds.hogwarts'],
},
{ id: 'bg5', file: 'library.jpg', name: enTranslations['backgroundEffects.backgrounds.library'] },
{
id: 'bg6',
file: 'new-york.jpg',
name: enTranslations['backgroundEffects.backgrounds.newYork'],
},
{ id: 'bg7', file: 'plane.jpg', name: enTranslations['backgroundEffects.backgrounds.plane'] },
];

const mockDeleteImageFromStorage = vi.fn();
const mockGetImagesFromStorage = vi.fn(() => customImages);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { ReactElement, useEffect, useState } from 'react';
import { Box, IconButton, Tooltip } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
import { BACKGROUNDS_PATH } from '../../../utils/constants';
import SelectableOption from '../SelectableOption';
import useImageStorage, { StoredImage } from '../../../utils/useImageStorage/useImageStorage';

export const backgrounds = [
{ id: 'bg1', file: 'bookshelf-room.jpg', name: 'Bookshelf Room' },
{ id: 'bg2', file: 'busy-room.jpg', name: 'Busy Room' },
{ id: 'bg3', file: 'dune-view.jpg', name: 'Dune View' },
{ id: 'bg4', file: 'hogwarts.jpg', name: 'Hogwarts' },
{ id: 'bg5', file: 'library.jpg', name: 'Library' },
{ id: 'bg6', file: 'new-york.jpg', name: 'New York' },
{ id: 'bg7', file: 'plane.jpg', name: 'Plane' },
{ id: 'bg8', file: 'white-room.jpg', name: 'White Room' },
];

export type BackgroundGalleryProps = {
backgroundSelected: string;
setBackgroundSelected: (dataUrl: string) => void;
Expand All @@ -39,6 +29,22 @@ const BackgroundGallery = ({
}: BackgroundGalleryProps): ReactElement => {
const { getImagesFromStorage, deleteImageFromStorage } = useImageStorage();
const [customImages, setCustomImages] = useState<StoredImage[]>([]);
const { t } = useTranslation();

const backgrounds = [
{
id: 'bg1',
file: 'bookshelf-room.jpg',
name: t('backgroundEffects.backgrounds.bookshelfRoom'),
},
{ id: 'bg2', file: 'busy-room.jpg', name: t('backgroundEffects.backgrounds.busyRoom') },
{ id: 'bg3', file: 'dune-view.jpg', name: t('backgroundEffects.backgrounds.duneView') },
{ id: 'bg4', file: 'hogwarts.jpg', name: t('backgroundEffects.backgrounds.hogwarts') },
{ id: 'bg5', file: 'library.jpg', name: t('backgroundEffects.backgrounds.library') },
{ id: 'bg6', file: 'new-york.jpg', name: t('backgroundEffects.backgrounds.newYork') },
{ id: 'bg7', file: 'plane.jpg', name: t('backgroundEffects.backgrounds.plane') },
{ id: 'bg8', file: 'white-room.jpg', name: t('backgroundEffects.backgrounds.whiteRoom') },
];

useEffect(() => {
setCustomImages(getImagesFromStorage());
Expand Down Expand Up @@ -67,22 +73,22 @@ const BackgroundGallery = ({
>
<SelectableOption
id={id}
title="Your Background"
title={t('backgroundEffects.yourBackground')}
isSelected={isSelected}
onClick={() => setBackgroundSelected(dataUrl)}
image={dataUrl}
>
<Tooltip
title={
isSelected
? "You can't remove this background while it's in use"
: 'Delete background'
? t('backgroundEffects.deleteTooltipInUse')
: t('backgroundEffects.deleteTooltip')
}
arrow
>
<IconButton
data-testid={`background-delete-${id}`}
aria-label="Delete custom background"
aria-label={t('backgroundEffects.deleteAriaLabel')}
onClick={(e) => {
e.stopPropagation();
if (!isSelected) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { ReactElement } from 'react';
import BlockIcon from '@mui/icons-material/Block';
import BlurOnIcon from '@mui/icons-material/BlurOn';
import { useTranslation } from 'react-i18next';
import SelectableOption from '../SelectableOption';

const options = [
{ key: 'none', icon: <BlockIcon sx={{ fontSize: '30px' }} />, name: 'Remove background' },
{ key: 'low-blur', icon: <BlurOnIcon />, name: 'Slight background blur' },
{
key: 'high-blur',
icon: <BlurOnIcon sx={{ fontSize: '30px' }} />,
name: 'Strong background blur',
},
];

export type EffectOptionButtonsProps = {
backgroundSelected: string;
setBackgroundSelected: (key: string) => void;
Expand All @@ -31,6 +22,20 @@ const EffectOptionButtons = ({
backgroundSelected,
setBackgroundSelected,
}: EffectOptionButtonsProps): ReactElement => {
const { t } = useTranslation();
const options = [
{
key: 'none',
icon: <BlockIcon sx={{ fontSize: '30px' }} />,
name: t('backgroundEffects.removeBackground'),
},
{ key: 'low-blur', icon: <BlurOnIcon />, name: t('backgroundEffects.slightBlur') },
{
key: 'high-blur',
icon: <BlurOnIcon sx={{ fontSize: '30px' }} />,
name: t('backgroundEffects.strongBlur'),
},
];
return (
<>
{options.map(({ key, icon, name }) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChangeEvent, useState, DragEvent, ReactElement } from 'react';
import { Box, Typography } from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { useTranslation } from 'react-i18next';
import { MAX_SIZE_MB } from '../../../utils/constants';

export type FileUploaderProps = {
Expand All @@ -19,6 +20,7 @@ export type FileUploaderProps = {
*/
const FileUploader = ({ handleFileChange }: FileUploaderProps): ReactElement => {
const [dragOver, setDragOver] = useState(false);
const { t } = useTranslation();

const onDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -69,9 +71,9 @@ const FileUploader = ({ handleFileChange }: FileUploaderProps): ReactElement =>
<>
<CloudUploadIcon sx={{ fontSize: 50, color: '#989A9D' }} />
<Typography className="file-upload-drop-area-text" mt={1}>
Drag and drop, or click here to upload image,
{t('backgroundEffects.dragDropText')}
<br />
Max {MAX_SIZE_MB}MB
{t('backgroundEffects.maxSize', { maxSize: MAX_SIZE_MB })}
</Typography>
</>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import usePublisherContext from '../../../hooks/usePublisherContext';
import DeviceControlButton from './DeviceControlButton';
import useConfigContext from '../../../hooks/useConfigContext';
import { ConfigContextType } from '../../../Context/ConfigProvider';
import enTranslations from '../../../locales/en.json';

vi.mock('../../../hooks/usePublisherContext.tsx');
vi.mock('../../../hooks/useSpeakingDetector.tsx');
Expand All @@ -22,6 +23,22 @@ vi.mock('../../../hooks/useBackgroundPublisherContext', () => {
});
vi.mock('../../../hooks/useConfigContext');

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'devices.audio.microphone.full': enTranslations['devices.audio.microphone.full'],
'devices.video.camera.full': enTranslations['devices.video.camera.full'],
'devices.settings.ariaLabel': enTranslations['devices.settings.ariaLabel'],
'devices.video.disabled': enTranslations['devices.video.disabled'],
'devices.audio.disabled': enTranslations['devices.audio.disabled'],
'mutedAlert.message.muted': enTranslations['mutedAlert.message.muted'],
};
return translations[key] || key;
},
}),
}));

const mockUsePublisherContext = usePublisherContext as Mock<[], PublisherContextType>;
const mockUseSpeakingDetector = useSpeakingDetector as Mock<[], boolean>;
const mockHandleToggleBackgroundEffects = vi.fn();
Expand Down Expand Up @@ -103,7 +120,7 @@ describe('DeviceControlButton', () => {
toggleBackgroundEffects={mockHandleToggleBackgroundEffects}
/>
);
const cameraButton = screen.getByLabelText('camera');
const cameraButton = screen.getByLabelText('Camera');
cameraButton.click();
expect(toggleVideoMock).toHaveBeenCalled();
expect(toggleBackgroundVideoPublisherMock).toHaveBeenCalled();
Expand All @@ -117,7 +134,7 @@ describe('DeviceControlButton', () => {
toggleBackgroundEffects={mockHandleToggleBackgroundEffects}
/>
);
const micButton = screen.getByLabelText('microphone');
const micButton = screen.getByLabelText('Microphone');
expect(micButton).toBeInTheDocument();
expect(micButton).not.toBeDisabled();

Expand All @@ -132,7 +149,7 @@ describe('DeviceControlButton', () => {
toggleBackgroundEffects={mockHandleToggleBackgroundEffects}
/>
);
const micButton = screen.getByLabelText('microphone');
const micButton = screen.getByLabelText('Microphone');
expect(micButton).toBeInTheDocument();
expect(micButton).toBeDisabled();

Expand All @@ -153,7 +170,7 @@ describe('DeviceControlButton', () => {
/>
);

const videoButton = screen.getByLabelText('camera');
const videoButton = screen.getByLabelText('Camera');
expect(videoButton).toBeInTheDocument();
expect(videoButton).not.toBeDisabled();

Expand All @@ -169,7 +186,7 @@ describe('DeviceControlButton', () => {
/>
);

const videoButton = screen.getByLabelText('camera');
const videoButton = screen.getByLabelText('Camera');
expect(videoButton).toBeInTheDocument();
expect(videoButton).toBeDisabled();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ const DeviceControlButton = ({
onClick={handleDeviceStateChange}
disabled={isButtonDisabled}
edge="start"
aria-label={isAudio ? 'microphone' : 'camera'}
aria-label={
isAudio ? t('devices.audio.microphone.full') : t('devices.video.camera.full')
}
size="small"
className="m-[3px] size-[50px] rounded-full shadow-md"
>
Expand Down
Loading
Loading