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
1 change: 1 addition & 0 deletions customWordList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Videocam
issuetype
Autolinker
NOSONAR
supportedLngs
5 changes: 5 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ VITE_BYPASS_WAITING_ROOM=false
# For testing with multiple devices, you can use ngrok to expose your vite server to the internet.
# Please refer to https://ngrok.com/docs/ for more information.
VITE_TUNNEL_DOMAIN=''

# Default language for i18next
VITE_I18N_FALLBACK_LANGUAGE='en'
# List all supported language, separated by | (example: VITE_I18N_SUPPORTED_LANGUAGES='en|fr|de|it')
VITE_I18N_SUPPORTED_LANGUAGES='en'
2 changes: 1 addition & 1 deletion frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ module.exports = {
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'react/require-default-props': 'off'
'react/require-default-props': 'off',
},
};
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
"autoprefixer": "^10.4.19",
"axios": "^1.12.0",
"events": "^3.3.0",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"lodash": "^4.17.21",
"opentok-layout-js": "^5.4.0",
"opentok-solutions-logging": "^1.1.5",
"postcss": "^8.4.38",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.1",
"react-router-dom": "^6.11.0",
"resize-observer-polyfill": "^1.5.1",
"tailwindcss": "^3.4.14",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import useUserContext from '../../../hooks/useUserContext';
import { UserContextType } from '../../user';
import useSessionContext from '../../../hooks/useSessionContext';
import { SessionContextType } from '../../SessionProvider/session';
import { PUBLISHING_BLOCKED_CAPTION } from '../../../utils/constants';

vi.mock('@vonage/client-sdk-video');
vi.mock('../../../hooks/useUserContext.tsx');
Expand Down Expand Up @@ -240,7 +239,8 @@ describe('usePublisher', () => {

const publishingBlockedError = {
header: 'Difficulties joining room',
caption: PUBLISHING_BLOCKED_CAPTION,
caption:
"We're having trouble connecting you with others in the meeting room. Please check your network and try again.",
};
expect(result.current.publishingError).toEqual(publishingBlockedError);
expect(mockedSessionPublish).toHaveBeenCalledTimes(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@ import OT, {
ExceptionEvent,
PublisherProperties,
} from '@vonage/client-sdk-video';
import { useTranslation } from 'react-i18next';
import usePublisherQuality, { NetworkQuality } from '../usePublisherQuality/usePublisherQuality';
import usePublisherOptions from '../usePublisherOptions';
import useSessionContext from '../../../hooks/useSessionContext';
import { PUBLISHING_BLOCKED_CAPTION } from '../../../utils/constants';
import getAccessDeniedError, {
PublishingErrorType,
} from '../../../utils/getAccessDeniedError/getAccessDeniedError';
import applyBackgroundFilter from '../../../utils/backgroundFilter/applyBackgroundFilter/applyBackgroundFilter';

type PublisherStreamCreatedEvent = Event<'streamCreated', Publisher> & {
Expand All @@ -29,6 +26,11 @@ type DeviceAccessStatus = {
camera: boolean | undefined;
};

type PublishingErrorType = {
header: string;
caption: string;
} | null;

export type AccessDeniedEvent = Event<'accessDenied', Publisher> & {
message?: string;
};
Expand Down Expand Up @@ -72,6 +74,7 @@ export type PublisherContextType = {
* @returns {PublisherContextType} the publisher context
*/
const usePublisher = (): PublisherContextType => {
const { t } = useTranslation();
const [publisherVideoElement, setPublisherVideoElement] = useState<
HTMLVideoElement | HTMLObjectElement
>();
Expand All @@ -96,10 +99,13 @@ const usePublisher = (): PublisherContextType => {
useEffect(() => {
if (deviceAccess?.microphone === false || deviceAccess?.camera === false) {
const device = deviceAccess.camera ? 'Microphone' : 'Camera';
const accessDeniedError = getAccessDeniedError(device);
const accessDeniedError = {
header: t('publishingErrors.accessDenied.title', { device }),
caption: t('publishingErrors.accessDenied.message', { device: device.toLowerCase() }),
};
setPublishingError(accessDeniedError);
}
}, [deviceAccess]);
}, [deviceAccess, t]);

useEffect(() => {
if (!publisherOptions) {
Expand Down Expand Up @@ -232,8 +238,8 @@ const usePublisher = (): PublisherContextType => {

if (publishAttempt === 3) {
const publishingBlocked: PublishingErrorType = {
header: 'Difficulties joining room',
caption: PUBLISHING_BLOCKED_CAPTION,
header: t('publishingErrors.blocked.title'),
caption: t('publishingErrors.blocked.message'),
};
setPublishingError(publishingBlocked);
setIsPublishingToSession(false);
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/api/archiving/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ export type ArchiveResponse = {

/**
* Returns a list of archives and the status of the archives for a given meeting room.
* @param {string} locale - current locale
* @param {string} roomName - The roomName we check for archives
* @returns {Promise<ArchiveResponse>} The archives from the meeting room (if any) and whether any archives are pending.
*/
const getArchives = async (roomName: string): Promise<ArchiveResponse> => {
const getArchives = async (locale: string, roomName: string): Promise<ArchiveResponse> => {
const response = await listArchives(roomName);
const archivesFromServer = response?.data?.archives;
if (archivesFromServer instanceof Array) {
const archives = archivesFromServer.map((archiveFromServer) =>
createArchiveFromServer(archiveFromServer)
createArchiveFromServer(locale, archiveFromServer)
);
return {
archives,
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/api/archiving/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export type Archive = {
createdAtFormatted: string;
};

const getDateString = (timestamp: number) => {
return `${getFormattedDate(timestamp)} ${getFormattedTime(timestamp)}`;
const getDateString = (locale: string, timestamp: number) => {
return `${getFormattedDate(locale, timestamp)} ${getFormattedTime(locale, timestamp)}`;
};

const getArchiveStatus = (status: ServerArchiveStatus): ArchiveStatus => {
Expand All @@ -47,16 +47,17 @@ const getArchiveStatus = (status: ServerArchiveStatus): ArchiveStatus => {

/**
* Modifies an archive retrieved from the server to be easily consumable.
* @param {string} locale - current locale
* @param {ServerArchive} serverArchive - The archive to be modified.
* @returns {Archive} The modified archive.
*/
export const createArchiveFromServer = (serverArchive: ServerArchive): Archive => {
export const createArchiveFromServer = (locale: string, serverArchive: ServerArchive): Archive => {
return {
id: serverArchive.id,
url: serverArchive.url,
status: getArchiveStatus(serverArchive.status),
createdAt: serverArchive.createdAt,
createdAtFormatted: getDateString(serverArchive.createdAt),
createdAtFormatted: getDateString(locale, serverArchive.createdAt),
};
};

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/api/archiving/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const mockListArchives = listArchives as Mock<[], ReturnType<typeof listArchives
describe('getArchives', () => {
it('it returns an object with array of Archives and hasPending flag', async () => {
mockListArchives.mockResolvedValue(mockResponse);
const archives = await getArchives('roomName');
const archives = await getArchives('en', 'roomName');
expect(archives).toEqual({
archives: [
{
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('getArchives', () => {
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as unknown as AxiosResponse<any, any>);
const archives = await getArchives('roomName');
const archives = await getArchives('en', 'roomName');
expect(archives).toEqual({
archives: [],
hasPending: false,
Expand All @@ -61,6 +61,6 @@ describe('getArchives', () => {

it('it throws with error when api call throws', async () => {
mockListArchives.mockRejectedValue(new AxiosError('Network Error', 'ERR_NETWORK'));
expect(getArchives('roomName')).rejects.toThrowError();
expect(getArchives('en', 'roomName')).rejects.toThrowError();
});
});
32 changes: 16 additions & 16 deletions frontend/src/api/archiving/tests/model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { availableServerArchive, failedServerArchive, startedServerArchive } fro

describe('createArchiveFromServer', () => {
it('should convert fields to model fields', () => {
const archive = createArchiveFromServer(availableServerArchive);
const archive = createArchiveFromServer('en', availableServerArchive);
expect(archive).toEqual({
createdAt: 1725268141000,
createdAtFormatted: 'Mon, Sep 2 5:09 AM',
Expand All @@ -15,27 +15,27 @@ describe('createArchiveFromServer', () => {
});

it('should return status as failed for failed and expired archives', () => {
expect(createArchiveFromServer(failedServerArchive).status).toBe('failed');
expect(createArchiveFromServer({ ...failedServerArchive, status: 'expired' }).status).toBe(
'failed'
);
expect(createArchiveFromServer('en', failedServerArchive).status).toBe('failed');
expect(
createArchiveFromServer('en', { ...failedServerArchive, status: 'expired' }).status
).toBe('failed');
});

it('should return available as failed for status available archives', () => {
expect(createArchiveFromServer(availableServerArchive).status).toBe('available');
expect(createArchiveFromServer('en', availableServerArchive).status).toBe('available');
});

it('should return status as pending for started, stopped, uploaded, and paused archives', () => {
expect(createArchiveFromServer(startedServerArchive).status).toBe('pending');
expect(createArchiveFromServer({ ...startedServerArchive, status: 'stopped' }).status).toBe(
'pending'
);
expect(createArchiveFromServer({ ...startedServerArchive, status: 'uploaded' }).status).toBe(
'pending'
);
expect(createArchiveFromServer({ ...startedServerArchive, status: 'paused' }).status).toBe(
'pending'
);
expect(createArchiveFromServer('en', startedServerArchive).status).toBe('pending');
expect(
createArchiveFromServer('en', { ...startedServerArchive, status: 'stopped' }).status
).toBe('pending');
expect(
createArchiveFromServer('en', { ...startedServerArchive, status: 'uploaded' }).status
).toBe('pending');
expect(
createArchiveFromServer('en', { ...startedServerArchive, status: 'paused' }).status
).toBe('pending');
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReactElement } from 'react';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import SelectableOption from '../SelectableOption';

export type AddBackgroundEffectProps = {
Expand All @@ -16,16 +17,17 @@ export type AddBackgroundEffectProps = {
* @returns {ReactElement} A button for uploading background effects.
*/
const AddBackgroundEffect = ({ isDisabled = false }: AddBackgroundEffectProps): ReactElement => {
const { t } = useTranslation();
return (
<Tooltip
title={
isDisabled ? (
'You have reached the maximum custom images limit'
t('backgroundEffects.limit')
) : (
<>
Recommended: JPG/PNG img. at 1280x720 resolution.
{t('backgroundEffects.recommended.specs')}
<br />
Note: Images are stored only locally in the browser.
{t('backgroundEffects.recommended.note')}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState, useEffect, ReactElement } from 'react';
import { CircularProgress, useMediaQuery } from '@mui/material';
import { useTranslation } from 'react-i18next';
import waitUntilPlaying from '../../../utils/waitUntilPlaying';

export type BackgroundVideoContainerProps = {
Expand All @@ -21,6 +22,7 @@ const BackgroundVideoContainer = ({
publisherVideoElement,
isParentVideoEnabled = false,
}: BackgroundVideoContainerProps): ReactElement => {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const [isVideoLoading, setIsVideoLoading] = useState<boolean>(true);
const isSMViewport = useMediaQuery(`(max-width:500px)`);
Expand Down Expand Up @@ -75,7 +77,7 @@ const BackgroundVideoContainer = ({
<div data-testid="background-video-container">
{!isParentVideoEnabled && (
<div className="background-video-container-disabled" style={{ width: containerWidth }}>
You have not enabled video
{t('backgroundEffects.video.disabled')}
</div>
)}
{isParentVideoEnabled && <div ref={containerRef} />}
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/DeviceAccessAlert/DeviceAccessAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { AlertTitle, Box, Dialog, Stack, Alert } from '@mui/material';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { DEVICE_ACCESS_STATUS } from '../../utils/constants';
import { isWebKit } from '../../utils/util';

const askDeviceMessage =
'To join the video room, your browser will request access to your camera and microphone.';
const deniedDeviceMessage =
"It seems your browser is blocked from accessing your camera and/or microphone. Reset the permission state through your browser's UI.";

export type DeviceAccessAlertProps = {
accessStatus: string | null;
};
Expand All @@ -20,8 +16,11 @@ export type DeviceAccessAlertProps = {
* @returns {ReactElement | false} - The rendered DeviceAccessAlert component if not Safari
*/
const DeviceAccessAlert = ({ accessStatus }: DeviceAccessAlertProps): ReactElement | false => {
const { t } = useTranslation();
const messageToDisplay =
accessStatus === DEVICE_ACCESS_STATUS.PENDING ? askDeviceMessage : deniedDeviceMessage;
accessStatus === DEVICE_ACCESS_STATUS.PENDING
? t('deviceAccessAlert.askDeviceMessage')
: t('deviceAccessAlert.deniedDeviceMessage');
const imgToDisplay =
accessStatus === DEVICE_ACCESS_STATUS.PENDING
? '/images/access-dialog-pending.png'
Expand Down Expand Up @@ -59,7 +58,7 @@ const DeviceAccessAlert = ({ accessStatus }: DeviceAccessAlertProps): ReactEleme
<Box
component="img"
src={imgToDisplay}
alt="Access Dialog"
alt={t('deviceAccessAlert.imageAlt')}
sx={{ maxWidth: 300, borderRadius: 1 }}
/>
</Box>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/GHRepoButton/GHRepoButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IconButton, Link, Tooltip } from '@mui/material';
import { GitHub as GitHubIcon } from '@mui/icons-material';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

/**
* GHRepoButton Component
Expand All @@ -9,9 +10,11 @@ import { ReactElement } from 'react';
* @returns {ReactElement} - The GHRepoButton component.
*/
const GHRepoButton = (): ReactElement => {
const { t } = useTranslation();

return (
<Link href="https://github.com/Vonage/vonage-video-react-app/" target="_blank">
<Tooltip title="Visit our GitHub Repo">
<Tooltip title={t('githubTooltip')}>
<IconButton color="default">
<GitHubIcon />
</IconButton>
Expand Down
Loading