From 013ea86b6d9fcc166e44131e1ab07ad9223bf46e Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:24:30 -0700 Subject: [PATCH 01/58] first things first --- config.example.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config.example.json diff --git a/config.example.json b/config.example.json new file mode 100644 index 00000000..398d2517 --- /dev/null +++ b/config.example.json @@ -0,0 +1,9 @@ +{ + "videoSettings": { + "enableDisableCapableCamera": true, + "resolution": "1080p", + "videoOnJoin": true, + "backgroundEffects": true + }, + "layoutMode": "active-speaker" +} From 44c7ab0a6c03e0e1de953f2ad40edb6c3f40ee09 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:50:04 -0700 Subject: [PATCH 02/58] working for some stuff --- frontend/src/App.tsx | 61 ++++++++++--------- frontend/src/Context/ConfigProvider/index.tsx | 20 ++++++ .../useConfigProvider/index.tsx | 4 ++ .../useConfigProvider/useConfigProvider.tsx | 50 +++++++++++++++ .../src/Context/SessionProvider/session.tsx | 4 +- frontend/src/hooks/useConfigContext.ts | 14 +++++ 6 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 frontend/src/Context/ConfigProvider/index.tsx create mode 100644 frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx create mode 100644 frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx create mode 100644 frontend/src/hooks/useConfigContext.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d25df311..f11bcf6d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,42 +12,45 @@ import RedirectToWaitingRoom from './components/RedirectToWaitingRoom'; import UnsupportedBrowserPage from './pages/UnsupportedBrowserPage'; import RoomContext from './Context/RoomContext'; import { BackgroundPublisherProvider } from './Context/BackgroundPublisherProvider'; +import { ConfigProvider } from './Context/ConfigProvider'; const App = () => { return ( - - - }> - + + + + }> + - - - + + + } - /> - - - - + /> + + + + - + - - - } - /> - - } /> - } /> - } /> - - + + + } + /> + + } /> + } /> + } /> + + + ); }; diff --git a/frontend/src/Context/ConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/index.tsx new file mode 100644 index 00000000..140162d9 --- /dev/null +++ b/frontend/src/Context/ConfigProvider/index.tsx @@ -0,0 +1,20 @@ +import { createContext, ReactNode, useMemo } from 'react'; +import useConfig, { defaultConfig } from './useConfigProvider'; + +export type ConfigProviderProps = { + children: ReactNode; +}; + +export type ConfigContextType = ReturnType; + +export const ConfigContext = createContext({ + videoSettings: defaultConfig.videoSettings, + layoutMode: defaultConfig.layoutMode, +}); + +export const ConfigContextProvider = ({ children }: ConfigProviderProps) => { + const config = useConfig(); + const value = useMemo(() => config, [config]); + + return {children}; +}; diff --git a/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx new file mode 100644 index 00000000..82c7dddc --- /dev/null +++ b/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx @@ -0,0 +1,4 @@ +import useConfig, { defaultConfig } from './useConfigProvider'; + +export { defaultConfig }; +export default useConfig; diff --git a/frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx b/frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx new file mode 100644 index 00000000..e4acaa90 --- /dev/null +++ b/frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +// eslint-disable-next-line import/no-relative-packages +import configFile from '../../../../../config.json'; + +export type VideoSettings = { + enableDisableCapableCamera: boolean; + resolution: '360p' | '480p' | '720p' | '1080p'; + videoOnJoin: boolean; + backgroundEffects: boolean; +}; + +export type AppConfig = { + videoSettings: VideoSettings; + layoutMode: 'grid' | 'active-speaker'; +}; + +export const defaultConfig: AppConfig = { + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1080p', + videoOnJoin: true, + backgroundEffects: true, + }, + layoutMode: 'active-speaker', +}; + +/** + * Hook wrapper for application configuration. Provides application configuration including video + * settings, layout preferences, etc. To configure settings, edit the + * `vonage-video-react-app/config.json` file. + * @returns {AppConfig} The application configuration + */ +const useConfig = (): AppConfig => { + const mergedConfig: AppConfig = useMemo(() => { + const typedConfigFile = configFile as Partial; + + return { + ...defaultConfig, + ...typedConfigFile, + videoSettings: { + ...defaultConfig.videoSettings, + ...(typedConfigFile.videoSettings || {}), + }, + }; + }, []); + + return mergedConfig; +}; + +export default useConfig; diff --git a/frontend/src/Context/SessionProvider/session.tsx b/frontend/src/Context/SessionProvider/session.tsx index 9d805ce4..3fc36c8e 100644 --- a/frontend/src/Context/SessionProvider/session.tsx +++ b/frontend/src/Context/SessionProvider/session.tsx @@ -13,6 +13,7 @@ import { import { Connection, Publisher, Stream } from '@vonage/client-sdk-video'; import fetchCredentials from '../../api/fetchCredentials'; import useUserContext from '../../hooks/useUserContext'; +import useConfigContext from '../../hooks/useConfigContext'; import ActiveSpeakerTracker from '../../utils/ActiveSpeakerTracker'; import useRightPanel, { RightPanelActiveTab } from '../../hooks/useRightPanel'; import { @@ -128,12 +129,13 @@ const MAX_PIN_COUNT = isMobile() ? MAX_PIN_COUNT_MOBILE : MAX_PIN_COUNT_DESKTOP; * @returns {SessionContextType} a context provider for a publisher preview */ const SessionProvider = ({ children }: SessionProviderProps): ReactElement => { + const { config } = useConfigContext(); const [lastStreamUpdate, setLastStreamUpdate] = useState(null); const vonageVideoClient = useRef(null); const [reconnecting, setReconnecting] = useState(false); const [subscriberWrappers, setSubscriberWrappers] = useState([]); const [ownCaptions, setOwnCaptions] = useState(null); - const [layoutMode, setLayoutMode] = useState('active-speaker'); + const [layoutMode, setLayoutMode] = useState(config.layoutMode); const [archiveId, setArchiveId] = useState(null); const activeSpeakerTracker = useRef(new ActiveSpeakerTracker()); const [activeSpeakerId, setActiveSpeakerId] = useState(); diff --git a/frontend/src/hooks/useConfigContext.ts b/frontend/src/hooks/useConfigContext.ts new file mode 100644 index 00000000..beede46b --- /dev/null +++ b/frontend/src/hooks/useConfigContext.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { ConfigContext, ConfigContextType } from '../Context/ConfigProvider'; + +/** + * Custom hook to access the Config context + * @returns {ConfigContextType} The config context value + */ +const useConfigContext = (): ConfigContextType => { + const context = useContext(ConfigContext); + + return context; +}; + +export default useConfigContext; From 426043944bb979a976f54dfea014ddb4da60f5cb Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:20:31 -0700 Subject: [PATCH 03/58] some renaming --- frontend/src/Context/ConfigProvider/index.tsx | 2 +- frontend/src/Context/ConfigProvider/useConfig/index.tsx | 4 ++++ .../useConfigProvider.tsx => useConfig/useConfig.tsx} | 0 .../src/Context/ConfigProvider/useConfigProvider/index.tsx | 4 ---- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 frontend/src/Context/ConfigProvider/useConfig/index.tsx rename frontend/src/Context/ConfigProvider/{useConfigProvider/useConfigProvider.tsx => useConfig/useConfig.tsx} (100%) delete mode 100644 frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx diff --git a/frontend/src/Context/ConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/index.tsx index 140162d9..10d42148 100644 --- a/frontend/src/Context/ConfigProvider/index.tsx +++ b/frontend/src/Context/ConfigProvider/index.tsx @@ -1,5 +1,5 @@ import { createContext, ReactNode, useMemo } from 'react'; -import useConfig, { defaultConfig } from './useConfigProvider'; +import useConfig, { defaultConfig } from './useConfig'; export type ConfigProviderProps = { children: ReactNode; diff --git a/frontend/src/Context/ConfigProvider/useConfig/index.tsx b/frontend/src/Context/ConfigProvider/useConfig/index.tsx new file mode 100644 index 00000000..f4720b90 --- /dev/null +++ b/frontend/src/Context/ConfigProvider/useConfig/index.tsx @@ -0,0 +1,4 @@ +import useConfig, { defaultConfig } from './useConfig'; + +export { defaultConfig }; +export default useConfig; diff --git a/frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx similarity index 100% rename from frontend/src/Context/ConfigProvider/useConfigProvider/useConfigProvider.tsx rename to frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx diff --git a/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx deleted file mode 100644 index 82c7dddc..00000000 --- a/frontend/src/Context/ConfigProvider/useConfigProvider/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import useConfig, { defaultConfig } from './useConfigProvider'; - -export { defaultConfig }; -export default useConfig; From 711f4b696e6093a3554c6b5cd5af416615c94766 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:33:19 -0700 Subject: [PATCH 04/58] enable/disable camera --- frontend/src/App.tsx | 6 +- .../usePreviewPublisher.tsx | 2 + .../src/Context/SessionProvider/session.tsx | 2 +- .../MeetingRoom/Toolbar/Toolbar.tsx | 6 +- .../WaitingRoom/CameraButton/CameraButton.tsx | 77 ++++++++++--------- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f11bcf6d..a26ba6b4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,11 +12,11 @@ import RedirectToWaitingRoom from './components/RedirectToWaitingRoom'; import UnsupportedBrowserPage from './pages/UnsupportedBrowserPage'; import RoomContext from './Context/RoomContext'; import { BackgroundPublisherProvider } from './Context/BackgroundPublisherProvider'; -import { ConfigProvider } from './Context/ConfigProvider'; +import { ConfigContextProvider } from './Context/ConfigProvider'; const App = () => { return ( - + }> @@ -50,7 +50,7 @@ const App = () => { } /> - + ); }; diff --git a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx index c00b9ebb..59d658c9 100644 --- a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx +++ b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx @@ -17,6 +17,7 @@ import { AccessDeniedEvent } from '../../PublisherProvider/usePublisher/usePubli import DeviceStore from '../../../utils/DeviceStore'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; import applyBackgroundFilter from '../../../utils/backgroundFilter/applyBackgroundFilter/applyBackgroundFilter'; +import useConfigContext from '../../../hooks/useConfigContext'; type PublisherVideoElementCreatedEvent = Event<'videoElementCreated', Publisher> & { element: HTMLVideoElement | HTMLObjectElement; @@ -66,6 +67,7 @@ export type PreviewPublisherContextType = { */ const usePreviewPublisher = (): PreviewPublisherContextType => { const { setUser, user } = useUserContext(); + const config = useConfigContext(); const { allMediaDevices, getAllMediaDevices } = useDevices(); const [publisherVideoElement, setPublisherVideoElement] = useState< HTMLVideoElement | HTMLObjectElement diff --git a/frontend/src/Context/SessionProvider/session.tsx b/frontend/src/Context/SessionProvider/session.tsx index 3fc36c8e..11f36121 100644 --- a/frontend/src/Context/SessionProvider/session.tsx +++ b/frontend/src/Context/SessionProvider/session.tsx @@ -129,7 +129,7 @@ const MAX_PIN_COUNT = isMobile() ? MAX_PIN_COUNT_MOBILE : MAX_PIN_COUNT_DESKTOP; * @returns {SessionContextType} a context provider for a publisher preview */ const SessionProvider = ({ children }: SessionProviderProps): ReactElement => { - const { config } = useConfigContext(); + const config = useConfigContext(); const [lastStreamUpdate, setLastStreamUpdate] = useState(null); const vonageVideoClient = useRef(null); const [reconnecting, setReconnecting] = useState(false); diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx index 1afdd407..787f0ba8 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx @@ -15,6 +15,7 @@ import EmojiGridButton from '../EmojiGridButton'; import isReportIssueEnabled from '../../../utils/isReportIssueEnabled'; import useToolbarButtons from '../../../hooks/useToolbarButtons'; import DeviceControlButton from '../DeviceControlButton'; +import useConfigContext from '../../../hooks/useConfigContext'; export type CaptionsState = { isUserCaptionsEnabled: boolean; @@ -69,6 +70,7 @@ const Toolbar = ({ captionsState, }: ToolbarProps): ReactElement => { const { disconnect, subscriberWrappers } = useSessionContext(); + const config = useConfigContext(); const isViewingScreenShare = subscriberWrappers.some((subWrapper) => subWrapper.isScreenshare); const isScreenSharePresent = isViewingScreenShare || isSharingScreen; const isPinningPresent = subscriberWrappers.some((subWrapper) => subWrapper.isPinned); @@ -168,10 +170,12 @@ const Toolbar = ({ deviceType="audio" toggleBackgroundEffects={toggleBackgroundEffects} /> - + )} {toolbarButtons.map(displayCenterToolbarButtons)}
diff --git a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx index 7a03ca20..fd956208 100644 --- a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx +++ b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx @@ -5,17 +5,20 @@ import { ReactElement } from 'react'; import usePreviewPublisherContext from '../../../hooks/usePreviewPublisherContext'; import VideoContainerButton from '../VideoContainerButton'; import useBackgroundPublisherContext from '../../../hooks/useBackgroundPublisherContext'; +import useConfigContext from '../../../hooks/useConfigContext'; /** * CameraButton Component * * Displays an overlay button to handle toggling video on and off for the preview publisher. - * @returns {ReactElement} - The CameraButton component. + * @returns {ReactElement | false} - The CameraButton component. */ -const CameraButton = (): ReactElement => { +const CameraButton = (): ReactElement | false => { const { isVideoEnabled, toggleVideo } = usePreviewPublisherContext(); const { toggleVideo: toggleBackgroundVideoPublisher } = useBackgroundPublisherContext(); + const config = useConfigContext(); const title = `Turn ${isVideoEnabled ? 'off' : 'on'} camera`; + const canEnableOrDisableCamera = config.videoSettings.enableDisableCapableCamera; const handleToggleVideo = () => { toggleVideo(); @@ -23,40 +26,42 @@ const CameraButton = (): ReactElement => { }; return ( - - - - ) : ( - - ) - } - /> - - + canEnableOrDisableCamera && ( + + + + ) : ( + + ) + } + /> + + + ) ); }; From 2d027d923bcd44534666fc2b1a605db676bddc66 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:44:50 -0700 Subject: [PATCH 05/58] video resolution --- config.example.json | 2 +- frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx | 4 ++-- .../usePreviewPublisher/usePreviewPublisher.tsx | 4 ++-- .../usePublisherOptions/usePublisherOptions.tsx | 6 ++++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config.example.json b/config.example.json index 398d2517..2421e7e9 100644 --- a/config.example.json +++ b/config.example.json @@ -1,7 +1,7 @@ { "videoSettings": { "enableDisableCapableCamera": true, - "resolution": "1080p", + "resolution": "1280x720", "videoOnJoin": true, "backgroundEffects": true }, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index e4acaa90..dc625a6d 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -4,7 +4,7 @@ import configFile from '../../../../../config.json'; export type VideoSettings = { enableDisableCapableCamera: boolean; - resolution: '360p' | '480p' | '720p' | '1080p'; + resolution: '1920x1080' | '1280x960' | '1280x720' | '640x480' | '640x360' | '320x240' | '320x180'; videoOnJoin: boolean; backgroundEffects: boolean; }; @@ -17,7 +17,7 @@ export type AppConfig = { export const defaultConfig: AppConfig = { videoSettings: { enableDisableCapableCamera: true, - resolution: '1080p', + resolution: '1280x720', videoOnJoin: true, backgroundEffects: true, }, diff --git a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx index 59d658c9..2fd3a3d6 100644 --- a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx +++ b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx @@ -242,7 +242,7 @@ const usePreviewPublisher = (): PreviewPublisherContextType => { const publisherOptions: PublisherProperties = { insertDefaultUI: false, videoFilter, - resolution: '1280x720', + resolution: config.videoSettings.resolution ?? '1280x720', audioSource, videoSource, }; @@ -256,7 +256,7 @@ const usePreviewPublisher = (): PreviewPublisherContextType => { } }); addPublisherListeners(publisherRef.current); - }, [addPublisherListeners]); + }, [addPublisherListeners, config.videoSettings.resolution]); /** * Destroys the preview publisher diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx index eb4b2d1a..c5686285 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx @@ -8,6 +8,7 @@ import { import useUserContext from '../../../hooks/useUserContext'; import getInitials from '../../../utils/getInitials'; import DeviceStore from '../../../utils/DeviceStore'; +import useConfigContext from '../../../hooks/useConfigContext'; /** * React hook to get PublisherProperties combining default options and options set in UserContext @@ -16,6 +17,7 @@ import DeviceStore from '../../../utils/DeviceStore'; const usePublisherOptions = (): PublisherProperties | null => { const { user } = useUserContext(); + const config = useConfigContext(); const [publisherOptions, setPublisherOptions] = useState(null); const deviceStoreRef = useRef(null); @@ -55,7 +57,7 @@ const usePublisherOptions = (): PublisherProperties | null => { name, publishAudio: !!publishAudio, publishVideo: !!publishVideo, - resolution: '1280x720', + resolution: config.videoSettings.resolution ?? '1280x720', audioFilter, videoFilter, videoSource, @@ -64,7 +66,7 @@ const usePublisherOptions = (): PublisherProperties | null => { }; setOptions(); - }, [user.defaultSettings]); + }, [config.videoSettings.resolution, user.defaultSettings]); return publisherOptions; }; From 85fcbdcfcc90f3936a77b6ba250d711b215086d5 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:46:40 -0700 Subject: [PATCH 06/58] less redundant --- .../usePreviewPublisher/usePreviewPublisher.tsx | 2 +- .../usePublisherOptions/usePublisherOptions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx index 2fd3a3d6..9ac49c67 100644 --- a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx +++ b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx @@ -242,7 +242,7 @@ const usePreviewPublisher = (): PreviewPublisherContextType => { const publisherOptions: PublisherProperties = { insertDefaultUI: false, videoFilter, - resolution: config.videoSettings.resolution ?? '1280x720', + resolution: config.videoSettings.resolution, audioSource, videoSource, }; diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx index c5686285..1a26c122 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx @@ -57,7 +57,7 @@ const usePublisherOptions = (): PublisherProperties | null => { name, publishAudio: !!publishAudio, publishVideo: !!publishVideo, - resolution: config.videoSettings.resolution ?? '1280x720', + resolution: config.videoSettings.resolution, audioFilter, videoFilter, videoSource, From 69680ca848795d0514ddf0933a9de61149cba391 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:22:15 -0700 Subject: [PATCH 07/58] .. --- frontend/src/App.tsx | 8 ++++---- frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a26ba6b4..7cb356a5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,11 +24,11 @@ const App = () => { path="/waiting-room/:roomName" element={ - + - } + } /> { - + - + } diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx index 787f0ba8..5abb4c8f 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx @@ -172,9 +172,9 @@ const Toolbar = ({ /> {config.videoSettings.enableDisableCapableCamera && ( + deviceType="video" + toggleBackgroundEffects={toggleBackgroundEffects} + /> )}
{toolbarButtons.map(displayCenterToolbarButtons)} From e695b8dfd92c922d1d90fdf782529f3dcb8c2f3a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:55:32 -0700 Subject: [PATCH 08/58] test fixes --- frontend/src/App.spec.tsx | 5 +++++ .../WaitingRoom/CameraButton/CameraButton.spec.tsx | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/frontend/src/App.spec.tsx b/frontend/src/App.spec.tsx index 6e23a5ca..37590a9a 100644 --- a/frontend/src/App.spec.tsx +++ b/frontend/src/App.spec.tsx @@ -30,6 +30,11 @@ vi.mock('./components/RedirectToWaitingRoom', () => ({ vi.mock('./Context/RoomContext', () => ({ default: ({ children }: React.PropsWithChildren) => children, })); +vi.mock('./Context/ConfigProvider', () => ({ + __esModule: true, + ConfigContextProvider: ({ children }: React.PropsWithChildren) => children, + default: ({ children }: React.PropsWithChildren) => children, +})); afterEach(() => { vi.clearAllMocks(); diff --git a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx index 917f3708..588ec43b 100644 --- a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx +++ b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx @@ -24,6 +24,16 @@ vi.mock('../../../hooks/useBackgroundPublisherContext', () => { }; }); +vi.mock('../../../hooks/useConfigContext', () => { + return { + default: () => ({ + videoSettings: { + enableDisableCapableCamera: true, + }, + }), + }; +}); + describe('CameraButton', () => { beforeEach(() => { isVideoEnabled = true; From 26ebf4232effeb221c6ff90da254a98828d3398a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:11:08 -0700 Subject: [PATCH 09/58] should work on gha --- .../ConfigProvider/useConfig/useConfig.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index dc625a6d..1adfcca9 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -1,6 +1,4 @@ import { useMemo } from 'react'; -// eslint-disable-next-line import/no-relative-packages -import configFile from '../../../../../config.json'; export type VideoSettings = { enableDisableCapableCamera: boolean; @@ -32,8 +30,17 @@ export const defaultConfig: AppConfig = { */ const useConfig = (): AppConfig => { const mergedConfig: AppConfig = useMemo(() => { - const typedConfigFile = configFile as Partial; - + if (process.env.NODE_ENV === 'test') { + return defaultConfig; + } + let typedConfigFile: Partial = {}; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports, global-require + typedConfigFile = require('../../../../../config.json') as Partial; + } catch (_e) { + // Fallback to defaultConfig if config.json is missing + typedConfigFile = {}; + } return { ...defaultConfig, ...typedConfigFile, From dc29859cc4bdc33e3d876ef5ee5811c00eba3554 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:16:58 -0700 Subject: [PATCH 10/58] that type --- config.moved.json | 9 +++++++++ .../src/Context/ConfigProvider/useConfig/useConfig.tsx | 3 ++- frontend/src/Context/SessionProvider/session.tsx | 3 +-- frontend/src/types/session.ts | 2 ++ 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 config.moved.json diff --git a/config.moved.json b/config.moved.json new file mode 100644 index 00000000..57bc167e --- /dev/null +++ b/config.moved.json @@ -0,0 +1,9 @@ +{ + "videoSettings": { + "enableDisableCapableCamera": false, + "resolution": "1280x720", + "videoOnJoin": true, + "backgroundEffects": true + }, + "layoutMode": "grid" +} diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 1adfcca9..58057a39 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { LayoutMode } from '../../../types/session'; export type VideoSettings = { enableDisableCapableCamera: boolean; @@ -9,7 +10,7 @@ export type VideoSettings = { export type AppConfig = { videoSettings: VideoSettings; - layoutMode: 'grid' | 'active-speaker'; + layoutMode: LayoutMode; }; export const defaultConfig: AppConfig = { diff --git a/frontend/src/Context/SessionProvider/session.tsx b/frontend/src/Context/SessionProvider/session.tsx index 11f36121..74dcd509 100644 --- a/frontend/src/Context/SessionProvider/session.tsx +++ b/frontend/src/Context/SessionProvider/session.tsx @@ -23,6 +23,7 @@ import { StreamPropertyChangedEvent, SubscriberAudioLevelUpdatedEvent, SubscriberWrapper, + LayoutMode, } from '../../types/session'; import useChat from '../../hooks/useChat'; import { ChatMessageType } from '../../types/chat'; @@ -37,8 +38,6 @@ import useEmoji, { EmojiWrapper } from '../../hooks/useEmoji'; export type { ChatMessageType } from '../../types/chat'; -export type LayoutMode = 'grid' | 'active-speaker'; - export type SessionContextType = { vonageVideoClient: null | VonageVideoClient; disconnect: null | (() => void); diff --git a/frontend/src/types/session.ts b/frontend/src/types/session.ts index 58aa443e..86ab7dbf 100644 --- a/frontend/src/types/session.ts +++ b/frontend/src/types/session.ts @@ -51,3 +51,5 @@ export type StreamPropertyChangedEvent = { oldValue: boolean | { width: number; height: number }; newValue: boolean | { width: number; height: number }; }; + +export type LayoutMode = 'grid' | 'active-speaker'; From 335a47f52cf5c1029421dfbc897ef8fa346f0cc5 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:43:25 -0700 Subject: [PATCH 11/58] revert and redo --- config.moved.json | 9 --------- .../ConfigProvider/useConfig/useConfig.tsx | 15 ++++----------- .../DeviceSettingsMenu/DeviceSettingsMenu.tsx | 4 +++- 3 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 config.moved.json diff --git a/config.moved.json b/config.moved.json deleted file mode 100644 index 57bc167e..00000000 --- a/config.moved.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "videoSettings": { - "enableDisableCapableCamera": false, - "resolution": "1280x720", - "videoOnJoin": true, - "backgroundEffects": true - }, - "layoutMode": "grid" -} diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 58057a39..926cedc2 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -1,4 +1,6 @@ import { useMemo } from 'react'; +// eslint-disable-next-line import/no-relative-packages +import configFile from '../../../../../config.json'; import { LayoutMode } from '../../../types/session'; export type VideoSettings = { @@ -31,17 +33,8 @@ export const defaultConfig: AppConfig = { */ const useConfig = (): AppConfig => { const mergedConfig: AppConfig = useMemo(() => { - if (process.env.NODE_ENV === 'test') { - return defaultConfig; - } - let typedConfigFile: Partial = {}; - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports, global-require - typedConfigFile = require('../../../../../config.json') as Partial; - } catch (_e) { - // Fallback to defaultConfig if config.json is missing - typedConfigFile = {}; - } + const typedConfigFile = configFile as Partial; + return { ...defaultConfig, ...typedConfigFile, diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx index a3eac284..bd2920f0 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx @@ -13,6 +13,7 @@ import useDropdownResizeObserver from '../../../hooks/useDropdownResizeObserver' import VideoDevices from '../VideoDevices'; import DropdownSeparator from '../DropdownSeparator'; import VideoDevicesOptions from '../VideoDevicesOptions'; +import useConfigContext from '../../../hooks/useConfigContext'; export type DeviceSettingsMenuProps = { deviceType: 'audio' | 'video'; @@ -51,6 +52,7 @@ const DeviceSettingsMenu = ({ handleClose, setIsOpen, }: DeviceSettingsMenuProps): ReactElement | false => { + const config = useConfigContext(); const isAudio = deviceType === 'audio'; const theme = useTheme(); const customLightBlueColor = 'rgb(138, 180, 248)'; @@ -71,7 +73,7 @@ const DeviceSettingsMenu = ({ return ( <> - {hasMediaProcessorSupport() && ( + {hasMediaProcessorSupport() && config.videoSettings.backgroundEffects && ( <> From 46f745fa67f45c07804da487826e9e3a93c0c1e1 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:47:59 -0700 Subject: [PATCH 12/58] lint issues --- .../src/utils/helpers/getLayoutBoxes/getLayoutElements.spec.ts | 3 +-- frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.spec.ts b/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.spec.ts index 04195ea5..095b41e5 100644 --- a/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.spec.ts +++ b/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.spec.ts @@ -1,8 +1,7 @@ import { Dimensions, Publisher } from '@vonage/client-sdk-video'; import { beforeEach, describe, expect, it } from 'vitest'; import getLayoutElementArray from './getLayoutElements'; -import { LayoutMode } from '../../../Context/SessionProvider/session'; -import { SubscriberWrapper } from '../../../types/session'; +import { LayoutMode, SubscriberWrapper } from '../../../types/session'; // Define unique values for width and height so we can identify layout elements const publisherWidth = 101; diff --git a/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.ts b/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.ts index 6b778cde..d26af37e 100644 --- a/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.ts +++ b/frontend/src/utils/helpers/getLayoutBoxes/getLayoutElements.ts @@ -1,8 +1,7 @@ import { Publisher, Subscriber } from '@vonage/client-sdk-video'; import { Box, Element } from 'opentok-layout-js'; import { MaybeElement } from '../../layoutManager'; -import { LayoutMode } from '../../../Context/SessionProvider/session'; -import { SubscriberWrapper } from '../../../types/session'; +import { LayoutMode, SubscriberWrapper } from '../../../types/session'; const isLayoutElement = (element: Element | MaybeElement): element is Element => { return element.width !== undefined && element.height !== undefined; From 4e30edd50efdfb64bdb2e247db98519cae359e68 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:53:28 -0700 Subject: [PATCH 13/58] should be good? --- frontend/src/vite-env.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 11f02fe2..faaeacf8 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +declare module "*.json" { + const value: unknown; + export default value; +} From a245d740c2d51dfd22fea6610bf20caef698bf92 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:10:07 -0700 Subject: [PATCH 14/58] should work on gha and local --- .../ConfigProvider/useConfig/useConfig.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 926cedc2..83aa6089 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -1,6 +1,4 @@ -import { useMemo } from 'react'; -// eslint-disable-next-line import/no-relative-packages -import configFile from '../../../../../config.json'; +import { useMemo, useEffect, useState } from 'react'; import { LayoutMode } from '../../../types/session'; export type VideoSettings = { @@ -32,8 +30,25 @@ export const defaultConfig: AppConfig = { * @returns {AppConfig} The application configuration */ const useConfig = (): AppConfig => { + const [config, setConfig] = useState(defaultConfig); + + useEffect(() => { + // Try to load config from JSON file located at src/public/config.json + const loadConfig = async () => { + try { + const response = await fetch('/config.json'); + const json = await response.json(); + setConfig(json); + } catch (error) { + console.info('Error loading config:', error); + } + }; + + loadConfig(); + }, []); + const mergedConfig: AppConfig = useMemo(() => { - const typedConfigFile = configFile as Partial; + const typedConfigFile = config as Partial; return { ...defaultConfig, @@ -43,7 +58,7 @@ const useConfig = (): AppConfig => { ...(typedConfigFile.videoSettings || {}), }, }; - }, []); + }, [config]); return mergedConfig; }; From de1d22449e86266bc799339cf36deaac6f0d65c1 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:46:37 -0700 Subject: [PATCH 15/58] ans configurable --- .../ConfigProvider/useConfig/useConfig.tsx | 10 ++++++++ .../ReduceNoiseTestSpeakers.spec.tsx | 24 +++++++++++++++++++ .../ReduceNoiseTestSpeakers.tsx | 5 +++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 83aa6089..f6296f7f 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -8,8 +8,14 @@ export type VideoSettings = { backgroundEffects: boolean; }; +export type AudioSettings = { + advancedNoiseSuppression: boolean; + audioOnJoin: boolean; +}; + export type AppConfig = { videoSettings: VideoSettings; + audioSettings: AudioSettings; layoutMode: LayoutMode; }; @@ -20,6 +26,10 @@ export const defaultConfig: AppConfig = { videoOnJoin: true, backgroundEffects: true, }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + }, layoutMode: 'active-speaker', }; diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx index 52203f83..3f739fb0 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx @@ -6,10 +6,14 @@ import { defaultAudioDevice } from '../../../utils/mockData/device'; import usePublisherContext from '../../../hooks/usePublisherContext'; import ReduceNoiseTestSpeakers from './ReduceNoiseTestSpeakers'; import { PublisherContextType } from '../../../Context/PublisherProvider'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/usePublisherContext'); +vi.mock('../../../hooks/useConfigContext'); const mockUsePublisherContext = usePublisherContext as Mock<[], PublisherContextType>; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; const { mockHasMediaProcessorSupport } = vi.hoisted(() => { return { @@ -23,6 +27,7 @@ vi.mock('@vonage/client-sdk-video', () => ({ describe('ReduceNoiseTestSpeakers', () => { let mockPublisher: Publisher; let publisherContext: PublisherContextType; + let configContext: ConfigContextType; beforeEach(() => { mockPublisher = Object.assign(new EventEmitter(), { @@ -43,7 +48,13 @@ describe('ReduceNoiseTestSpeakers', () => { publisherContext.publisher = mockPublisher; }) as unknown as () => void, } as unknown as PublisherContextType; + configContext = { + audioSettings: { + advancedNoiseSuppression: true, + }, + } as unknown as ConfigContextType; mockUsePublisherContext.mockImplementation(() => publisherContext); + mockUseConfigContext.mockReturnValue(configContext); }); afterEach(() => { @@ -128,4 +139,17 @@ describe('ReduceNoiseTestSpeakers', () => { expect(computedStyle.visibility).toBe('hidden'); }); }); + + it('should not render the Advanced Noise Suppression option if it is configured to be disabled', () => { + configContext = { + audioSettings: { + advancedNoiseSuppression: false, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); + + render(); + + expect(screen.queryByText('Advanced Noise Suppression')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx index 2f248375..9612942e 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx @@ -10,6 +10,7 @@ import usePublisherContext from '../../../hooks/usePublisherContext'; import DropdownSeparator from '../DropdownSeparator'; import SoundTest from '../../SoundTest'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; +import useConfigContext from '../../../hooks/useConfigContext'; export type ReduceNoiseTestSpeakersProps = { customLightBlueColor: string; @@ -28,7 +29,9 @@ const ReduceNoiseTestSpeakers = ({ customLightBlueColor, }: ReduceNoiseTestSpeakersProps): ReactElement | false => { const { publisher, isPublishing } = usePublisherContext(); + const config = useConfigContext(); const [isToggled, setIsToggled] = useState(false); + const isANSEnabled = config.audioSettings.advancedNoiseSuppression; const handleToggle = async () => { const newState = !isToggled; @@ -58,7 +61,7 @@ const ReduceNoiseTestSpeakers = ({ mt: 1, }} > - {hasMediaProcessorSupport() && ( + {hasMediaProcessorSupport() && isANSEnabled && ( Date: Thu, 4 Sep 2025 08:20:31 -0700 Subject: [PATCH 16/58] test --- .../DeviceSettingsMenu.spec.tsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx index 28a067f8..2878b6cd 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx @@ -12,6 +12,8 @@ import { nativeDevices, videoInputDevices, } from '../../../utils/mockData/device'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; const { mockHasMediaProcessorSupport, @@ -44,6 +46,9 @@ vi.mock('../../../utils/util', async () => { }; }); +vi.mock('../../../hooks/useConfigContext'); +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; + // This is returned by Vonage SDK if audioOutput is not supported const vonageDefaultEmptyOutputDevice = { deviceId: null, label: null }; @@ -58,6 +63,7 @@ describe('DeviceSettingsMenu Component', () => { const mockHandleClose = vi.fn(); let deviceChangeListener: EventEmitter; const mockedHasMediaProcessorSupport = vi.fn(); + let configContext: ConfigContextType; beforeEach(() => { vi.resetAllMocks(); @@ -82,6 +88,15 @@ describe('DeviceSettingsMenu Component', () => { }); (hasMediaProcessorSupport as Mock).mockImplementation(mockedHasMediaProcessorSupport); mockedHasMediaProcessorSupport.mockReturnValue(false); + configContext = { + audioSettings: { + enableDisableCapableCamera: true, + }, + videoSettings: { + backgroundEffects: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); }); afterAll(() => { @@ -350,5 +365,31 @@ describe('DeviceSettingsMenu Component', () => { expect(screen.queryByText('Background effects')).not.toBeInTheDocument(); }); }); + + it('does not render the dropdown separator and background effects option when the config has disabled background effects', async () => { + configContext = { + videoSettings: { + backgroundEffects: false, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); + + render( + + ); + + await waitFor(() => { + expect(screen.queryByTestId('dropdown-separator')).not.toBeInTheDocument(); + expect(screen.queryByText('Background effects')).not.toBeInTheDocument(); + }); + }); }); }); From cf5f4069fc32c5f952d48ae3bbb8446546d36295 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:35:45 -0700 Subject: [PATCH 17/58] audio/video control button tests --- .../ConfigProvider/useConfig/useConfig.tsx | 2 + .../MeetingRoom/Toolbar/Toolbar.spec.tsx | 41 +++++++++++++++++++ .../MeetingRoom/Toolbar/Toolbar.tsx | 14 ++++--- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index f6296f7f..534e7324 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -11,6 +11,7 @@ export type VideoSettings = { export type AudioSettings = { advancedNoiseSuppression: boolean; audioOnJoin: boolean; + enableDisableCapableMicrophone: boolean; }; export type AppConfig = { @@ -29,6 +30,7 @@ export const defaultConfig: AppConfig = { audioSettings: { advancedNoiseSuppression: true, audioOnJoin: true, + enableDisableCapableMicrophone: true, }, layoutMode: 'active-speaker', }; diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.spec.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.spec.tsx index 274f0281..bffe6693 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.spec.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.spec.tsx @@ -9,6 +9,8 @@ import useToolbarButtons, { UseToolbarButtonsProps, } from '../../../hooks/useToolbarButtons'; import { RIGHT_PANEL_BUTTON_COUNT } from '../../../utils/constants'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; const mockedRoomName = { roomName: 'test-room-name' }; @@ -21,6 +23,7 @@ vi.mock('react-router-dom', () => ({ vi.mock('../../../hooks/useSpeakingDetector'); vi.mock('../../../utils/isReportIssueEnabled'); vi.mock('../../../hooks/useToolbarButtons'); +vi.mock('../../../hooks/useConfigContext'); const mockUseSpeakingDetector = useSpeakingDetector as Mock<[], boolean>; const mockIsReportIssueEnabled = isReportIssueEnabled as Mock<[], boolean>; @@ -28,8 +31,11 @@ const mockUseToolbarButtons = useToolbarButtons as Mock< [UseToolbarButtonsProps], UseToolbarButtons >; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('Toolbar', () => { + let configContext: ConfigContextType; + beforeEach(() => { (useLocation as Mock).mockReturnValue({ state: mockedRoomName, @@ -46,6 +52,15 @@ describe('Toolbar', () => { return renderedToolbarButtons; } ); + configContext = { + audioSettings: { + enableDisableCapableMicrophone: true, + }, + videoSettings: { + enableDisableCapableCamera: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); }); afterAll(() => { @@ -114,4 +129,30 @@ describe('Toolbar', () => { expect(screen.queryByTestId('emoji-grid-button')).toBeVisible(); expect(screen.queryByTestId('captions-button')).toBeVisible(); }); + + describe('audio DeviceControlButton', () => { + it('is rendered when it is configured to be enabled', () => { + render(); + expect(screen.getByTestId('audio-dropdown-button')).toBeInTheDocument(); + }); + + it('is not rendered when it is configured to be disabled', () => { + configContext.audioSettings.enableDisableCapableMicrophone = false; + render(); + expect(screen.queryByTestId('audio-dropdown-button')).not.toBeInTheDocument(); + }); + }); + + describe('video DeviceControlButton', () => { + it('is rendered when it is configured to be enabled', () => { + render(); + expect(screen.getByTestId('video-dropdown-button')).toBeInTheDocument(); + }); + + it('is not rendered when it is configured to be disabled', () => { + configContext.videoSettings.enableDisableCapableCamera = false; + render(); + expect(screen.queryByTestId('video-dropdown-button')).not.toBeInTheDocument(); + }); + }); }); diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx index 5abb4c8f..46a19c7c 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx @@ -81,6 +81,8 @@ const Toolbar = ({ disconnect(); }, [disconnect]); const [openEmojiGridDesktop, setOpenEmojiGridDesktop] = useState(false); + const { enableDisableCapableMicrophone } = config.audioSettings; + const { enableDisableCapableCamera } = config.videoSettings; // An array of buttons available for the toolbar. As the toolbar resizes, buttons may be hidden and moved to the // ToolbarOverflowMenu to ensure a responsive layout without compromising usability. @@ -166,11 +168,13 @@ const Toolbar = ({
- - {config.videoSettings.enableDisableCapableCamera && ( + {enableDisableCapableMicrophone && ( + + )} + {enableDisableCapableCamera && ( Date: Thu, 4 Sep 2025 08:50:03 -0700 Subject: [PATCH 18/58] fix lint --- frontend/src/Context/ConfigProvider/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Context/ConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/index.tsx index 10d42148..7e332e15 100644 --- a/frontend/src/Context/ConfigProvider/index.tsx +++ b/frontend/src/Context/ConfigProvider/index.tsx @@ -10,6 +10,7 @@ export type ConfigContextType = ReturnType; export const ConfigContext = createContext({ videoSettings: defaultConfig.videoSettings, layoutMode: defaultConfig.layoutMode, + audioSettings: defaultConfig.audioSettings, }); export const ConfigContextProvider = ({ children }: ConfigProviderProps) => { From 020364f571098322c2566f313daf40505be970c5 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:51:44 -0700 Subject: [PATCH 19/58] fix tests --- frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx index 674e3c6c..bf1781c5 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx @@ -54,6 +54,18 @@ vi.mock('@mui/material', async () => { useMediaQuery: vi.fn(), }; }); +vi.mock('../../hooks/useConfigContext', () => { + return { + default: () => ({ + videoSettings: { + enableDisableCapableCamera: true, + }, + audioSettings: { + enableDisableCapableMicrophone: true, + }, + }), + }; +}); vi.mock('../../hooks/useDevices.tsx'); vi.mock('../../hooks/usePublisherContext.tsx'); From 7f4d686a7d3cbf90d81d178140bf16faf1398377 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:28:16 -0700 Subject: [PATCH 20/58] tests for config --- .../useConfig/useConfig.spec.tsx | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx new file mode 100644 index 00000000..9003bef8 --- /dev/null +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -0,0 +1,82 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import useConfig from './useConfig'; + +describe('useConfig', () => { + let nativeFetch: typeof global.fetch; + + beforeAll(() => { + nativeFetch = global.fetch; + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + afterAll(() => { + global.fetch = nativeFetch; + }); + + it('returns the default config when no config.json is loaded', () => { + const { result } = renderHook(() => useConfig()); + expect(result.current).toMatchObject({ + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1280x720', + videoOnJoin: true, + backgroundEffects: true, + }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + enableDisableCapableMicrophone: true, + }, + layoutMode: 'active-speaker', + }); + }); + + it('merges config.json values if loaded (mocked fetch)', async () => { + const mockConfig = { + videoSettings: { + enableDisableCapableCamera: false, + resolution: '640x480', + videoOnJoin: false, + backgroundEffects: false, + }, + audioSettings: { + advancedNoiseSuppression: false, + audioOnJoin: false, + enableDisableCapableMicrophone: false, + }, + layoutMode: 'grid', + }; + global.fetch = vi.fn().mockResolvedValue({ + json: async () => mockConfig, + }); + const { result } = renderHook(() => useConfig()); + + await waitFor(() => { + expect(result.current).toMatchObject(mockConfig); + }); + }); + + it('falls back to defaultConfig if fetch fails', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('mocking a failure to fetch')); + const { result } = renderHook(() => useConfig()); + + expect(result.current).toMatchObject({ + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1280x720', + videoOnJoin: true, + backgroundEffects: true, + }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + enableDisableCapableMicrophone: true, + }, + layoutMode: 'active-speaker', + }); + }); +}); From 9bef6245b7049dd96328e5c761d9aedf68dd7284 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:53:19 -0700 Subject: [PATCH 21/58] enable-disable waiting room --- .../WaitingRoom/MicButton/MicButton.tsx | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx b/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx index a6425a51..cf802588 100644 --- a/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx +++ b/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx @@ -4,53 +4,58 @@ import MicIcon from '@mui/icons-material/Mic'; import { ReactElement } from 'react'; import usePreviewPublisherContext from '../../../hooks/usePreviewPublisherContext'; import VideoContainerButton from '../VideoContainerButton'; +import useConfigContext from '../../../hooks/useConfigContext'; /** * MicButton Component * * Toggles the user's microphone (published audio) and updates the icon accordingly. - * @returns {ReactElement} - The MicButton component. + * @returns {ReactElement | false} - The MicButton component. */ -const MicButton = (): ReactElement => { +const MicButton = (): ReactElement | false => { const { isAudioEnabled, toggleAudio } = usePreviewPublisherContext(); + const config = useConfigContext(); const title = `Turn ${isAudioEnabled ? 'off' : 'on'} microphone`; + const canEnableOrDisableMicrophone = config.audioSettings.enableDisableCapableMicrophone; return ( - - - - ) : ( - - ) - } - /> - - + canEnableOrDisableMicrophone && ( + + + + ) : ( + + ) + } + /> + + + ) ); }; From 6e699912eeef12895f440bd8216cde45ea4faa45 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:14:09 -0700 Subject: [PATCH 22/58] cleaner implementation --- .../DeviceControlButton.spec.tsx | 112 ++++++++++++++---- .../DeviceControlButton.tsx | 46 +++++-- .../MeetingRoom/Toolbar/Toolbar.spec.tsx | 41 ------- .../MeetingRoom/Toolbar/Toolbar.tsx | 24 ++-- 4 files changed, 133 insertions(+), 90 deletions(-) diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx index 874243fe..9c50c81b 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, Mock, afterEach, afterAll } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Publisher } from '@vonage/client-sdk-video'; import { EventEmitter } from 'stream'; import { PublisherContextType } from '../../../Context/PublisherProvider'; @@ -7,6 +7,8 @@ import { defaultAudioDevice } from '../../../utils/mockData/device'; import useSpeakingDetector from '../../../hooks/useSpeakingDetector'; import usePublisherContext from '../../../hooks/usePublisherContext'; import DeviceControlButton from './DeviceControlButton'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/usePublisherContext.tsx'); vi.mock('../../../hooks/useSpeakingDetector.tsx'); @@ -18,15 +20,19 @@ vi.mock('../../../hooks/useBackgroundPublisherContext', () => { }), }; }); +vi.mock('../../../hooks/useConfigContext'); const mockUsePublisherContext = usePublisherContext as Mock<[], PublisherContextType>; const mockUseSpeakingDetector = useSpeakingDetector as Mock<[], boolean>; const mockHandleToggleBackgroundEffects = vi.fn(); +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('DeviceControlButton', () => { const nativeMediaDevices = global.navigator.mediaDevices; let mockPublisher: Publisher; let publisherContext: PublisherContextType; + let configContext: ConfigContextType; + beforeEach(() => { mockPublisher = Object.assign(new EventEmitter(), { applyVideoFilter: vi.fn(), @@ -59,6 +65,16 @@ describe('DeviceControlButton', () => { removeEventListener: vi.fn(() => []), }, }); + + configContext = { + audioSettings: { + enableDisableCapableMicrophone: true, + }, + videoSettings: { + enableDisableCapableCamera: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); }); afterEach(() => { @@ -72,28 +88,6 @@ describe('DeviceControlButton', () => { }); }); - it('renders the video control button', () => { - render( - - ); - expect(screen.getByLabelText('camera')).toBeInTheDocument(); - expect(screen.getByTestId('ArrowDropUpIcon')).toBeInTheDocument(); - }); - - it('renders the audio control button', () => { - render( - - ); - expect(screen.getByLabelText('microphone')).toBeInTheDocument(); - expect(screen.getByTestId('ArrowDropUpIcon')).toBeInTheDocument(); - }); - it('updates the main publisher and the background replacement publisher when clicked', () => { const toggleVideoMock = vi.fn(); mockUsePublisherContext.mockImplementation(() => ({ @@ -114,4 +108,76 @@ describe('DeviceControlButton', () => { expect(toggleVideoMock).toHaveBeenCalled(); expect(toggleBackgroundVideoPublisherMock).toHaveBeenCalled(); }); + + describe('audio DeviceControlButton', () => { + it('is not disabled when it is configured to be enabled', () => { + render( + + ); + const micButton = screen.getByLabelText('microphone'); + expect(micButton).toBeInTheDocument(); + expect(micButton).not.toBeDisabled(); + + expect(screen.getByTestId('ArrowDropUpIcon')).toBeInTheDocument(); + }); + + it('renders the button as disabled with greyed out icon and correct tooltip when microphone control is disabled', async () => { + configContext.audioSettings.enableDisableCapableMicrophone = false; + render( + + ); + const micButton = screen.getByLabelText('microphone'); + expect(micButton).toBeInTheDocument(); + expect(micButton).toBeDisabled(); + + const tooltip = screen.getByLabelText('device settings'); + fireEvent.mouseOver(tooltip); + expect( + await screen.findByText('Microphone control is disabled in this application') + ).toBeInTheDocument(); + }); + }); + + describe('video DeviceControlButton', () => { + it('is rendered when it is configured to be enabled', () => { + render( + + ); + + const videoButton = screen.getByLabelText('camera'); + expect(videoButton).toBeInTheDocument(); + expect(videoButton).not.toBeDisabled(); + + expect(screen.getByTestId('ArrowDropUpIcon')).toBeInTheDocument(); + }); + + it('is not rendered when it is configured to be disabled', async () => { + configContext.videoSettings.enableDisableCapableCamera = false; + render( + + ); + + const videoButton = screen.getByLabelText('camera'); + expect(videoButton).toBeInTheDocument(); + expect(videoButton).toBeDisabled(); + + const tooltip = screen.getByLabelText('device settings'); + fireEvent.mouseOver(tooltip); + expect( + await screen.findByText('Camera control is disabled in this application') + ).toBeInTheDocument(); + }); + }); }); diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index 57ea8a79..ceb2e944 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -10,6 +10,7 @@ import MutedAlert from '../../MutedAlert'; import usePublisherContext from '../../../hooks/usePublisherContext'; import DeviceSettingsMenu from '../DeviceSettingsMenu'; import useBackgroundPublisherContext from '../../../hooks/useBackgroundPublisherContext'; +import useConfigContext from '../../../hooks/useConfigContext'; export type DeviceControlButtonProps = { deviceType: 'audio' | 'video'; @@ -32,11 +33,27 @@ const DeviceControlButton = ({ }: DeviceControlButtonProps): ReactElement => { const { isVideoEnabled, toggleAudio, toggleVideo, isAudioEnabled } = usePublisherContext(); const { toggleVideo: toggleBackgroundVideoPublisher } = useBackgroundPublisherContext(); + const config = useConfigContext(); const isAudio = deviceType === 'audio'; const [open, setOpen] = useState(false); const anchorRef = useRef(null); const audioTitle = isAudioEnabled ? 'Disable microphone' : 'Enable microphone'; const videoTitle = isVideoEnabled ? 'Disable video' : 'Enable video'; + const { enableDisableCapableMicrophone } = config.audioSettings; + const { enableDisableCapableCamera } = config.videoSettings; + const isButtonDisabled = isAudio ? !enableDisableCapableMicrophone : !enableDisableCapableCamera; + let tooltipTitle: string; + if (isAudio) { + if (!enableDisableCapableMicrophone) { + tooltipTitle = 'Microphone control is disabled in this application'; + } else { + tooltipTitle = audioTitle; + } + } else if (!enableDisableCapableCamera) { + tooltipTitle = 'Camera control is disabled in this application'; + } else { + tooltipTitle = videoTitle; + } const handleToggle = () => { setOpen((prevOpen) => !prevOpen); @@ -51,12 +68,18 @@ const DeviceControlButton = ({ const renderControlIcon = () => { if (isAudio) { + if (!enableDisableCapableMicrophone) { + return ; + } if (isAudioEnabled) { return ; } return ; } + if (!enableDisableCapableCamera) { + return ; + } if (isVideoEnabled) { return ; } @@ -99,16 +122,19 @@ const DeviceControlButton = ({ )} - - - {renderControlIcon()} - + +
+ + {renderControlIcon()} + +
({ vi.mock('../../../hooks/useSpeakingDetector'); vi.mock('../../../utils/isReportIssueEnabled'); vi.mock('../../../hooks/useToolbarButtons'); -vi.mock('../../../hooks/useConfigContext'); const mockUseSpeakingDetector = useSpeakingDetector as Mock<[], boolean>; const mockIsReportIssueEnabled = isReportIssueEnabled as Mock<[], boolean>; @@ -31,11 +28,8 @@ const mockUseToolbarButtons = useToolbarButtons as Mock< [UseToolbarButtonsProps], UseToolbarButtons >; -const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('Toolbar', () => { - let configContext: ConfigContextType; - beforeEach(() => { (useLocation as Mock).mockReturnValue({ state: mockedRoomName, @@ -52,15 +46,6 @@ describe('Toolbar', () => { return renderedToolbarButtons; } ); - configContext = { - audioSettings: { - enableDisableCapableMicrophone: true, - }, - videoSettings: { - enableDisableCapableCamera: true, - }, - } as unknown as ConfigContextType; - mockUseConfigContext.mockReturnValue(configContext); }); afterAll(() => { @@ -129,30 +114,4 @@ describe('Toolbar', () => { expect(screen.queryByTestId('emoji-grid-button')).toBeVisible(); expect(screen.queryByTestId('captions-button')).toBeVisible(); }); - - describe('audio DeviceControlButton', () => { - it('is rendered when it is configured to be enabled', () => { - render(); - expect(screen.getByTestId('audio-dropdown-button')).toBeInTheDocument(); - }); - - it('is not rendered when it is configured to be disabled', () => { - configContext.audioSettings.enableDisableCapableMicrophone = false; - render(); - expect(screen.queryByTestId('audio-dropdown-button')).not.toBeInTheDocument(); - }); - }); - - describe('video DeviceControlButton', () => { - it('is rendered when it is configured to be enabled', () => { - render(); - expect(screen.getByTestId('video-dropdown-button')).toBeInTheDocument(); - }); - - it('is not rendered when it is configured to be disabled', () => { - configContext.videoSettings.enableDisableCapableCamera = false; - render(); - expect(screen.queryByTestId('video-dropdown-button')).not.toBeInTheDocument(); - }); - }); }); diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx index 46a19c7c..1afdd407 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx @@ -15,7 +15,6 @@ import EmojiGridButton from '../EmojiGridButton'; import isReportIssueEnabled from '../../../utils/isReportIssueEnabled'; import useToolbarButtons from '../../../hooks/useToolbarButtons'; import DeviceControlButton from '../DeviceControlButton'; -import useConfigContext from '../../../hooks/useConfigContext'; export type CaptionsState = { isUserCaptionsEnabled: boolean; @@ -70,7 +69,6 @@ const Toolbar = ({ captionsState, }: ToolbarProps): ReactElement => { const { disconnect, subscriberWrappers } = useSessionContext(); - const config = useConfigContext(); const isViewingScreenShare = subscriberWrappers.some((subWrapper) => subWrapper.isScreenshare); const isScreenSharePresent = isViewingScreenShare || isSharingScreen; const isPinningPresent = subscriberWrappers.some((subWrapper) => subWrapper.isPinned); @@ -81,8 +79,6 @@ const Toolbar = ({ disconnect(); }, [disconnect]); const [openEmojiGridDesktop, setOpenEmojiGridDesktop] = useState(false); - const { enableDisableCapableMicrophone } = config.audioSettings; - const { enableDisableCapableCamera } = config.videoSettings; // An array of buttons available for the toolbar. As the toolbar resizes, buttons may be hidden and moved to the // ToolbarOverflowMenu to ensure a responsive layout without compromising usability. @@ -168,18 +164,14 @@ const Toolbar = ({
- {enableDisableCapableMicrophone && ( - - )} - {enableDisableCapableCamera && ( - - )} + +
{toolbarButtons.map(displayCenterToolbarButtons)}
From 3142edcb69ac9ca32c040b1ab3f6643ac81c33f2 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:27:03 -0700 Subject: [PATCH 23/58] audio and video on join --- .../usePublisherOptions.spec.tsx | 42 +++++++++++++++++++ .../usePublisherOptions.tsx | 11 +++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx index 32c402f0..d2d5a383 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx @@ -7,10 +7,14 @@ import usePublisherOptions from './usePublisherOptions'; import localStorageMock from '../../../utils/mockData/localStorageMock'; import DeviceStore from '../../../utils/DeviceStore'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../ConfigProvider'; vi.mock('../../../hooks/useUserContext.tsx'); +vi.mock('../../../hooks/useConfigContext'); const mockUseUserContext = useUserContext as Mock<[], UserContextType>; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; const defaultSettings = { publishAudio: false, @@ -51,6 +55,8 @@ const mockUserContextWithCustomSettings = { describe('usePublisherOptions', () => { let enumerateDevicesMock: ReturnType; let deviceStore: DeviceStore; + let configContext: ConfigContextType; + beforeEach(async () => { enumerateDevicesMock = vi.fn(); vi.stubGlobal('navigator', { @@ -65,6 +71,16 @@ describe('usePublisherOptions', () => { deviceStore = new DeviceStore(); enumerateDevicesMock.mockResolvedValue([]); await deviceStore.init(); + configContext = { + audioSettings: { + audioOnJoin: true, + }, + videoSettings: { + resolution: '1280x720', + videoOnJoin: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); }); afterAll(() => { @@ -140,4 +156,30 @@ describe('usePublisherOptions', () => { }); }); }); + + describe('configurable features', () => { + it('should disable audio publishing', async () => { + configContext.audioSettings.audioOnJoin = false; + const { result } = renderHook(() => usePublisherOptions()); + await waitFor(() => { + expect(result.current?.publishAudio).toBe(false); + }); + }); + + it('should disable video publishing', async () => { + configContext.videoSettings.videoOnJoin = false; + const { result } = renderHook(() => usePublisherOptions()); + await waitFor(() => { + expect(result.current?.publishVideo).toBe(false); + }); + }); + + it('should configure resolution from config', async () => { + configContext.videoSettings.resolution = '640x480'; + const { result } = renderHook(() => usePublisherOptions()); + await waitFor(() => { + expect(result.current?.resolution).toBe('640x480'); + }); + }); + }); }); diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx index 1a26c122..b3bdeb9e 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx @@ -55,8 +55,8 @@ const usePublisherOptions = (): PublisherProperties | null => { initials, insertDefaultUI: false, name, - publishAudio: !!publishAudio, - publishVideo: !!publishVideo, + publishAudio: config.audioSettings.audioOnJoin && publishAudio, + publishVideo: config.videoSettings.videoOnJoin && publishVideo, resolution: config.videoSettings.resolution, audioFilter, videoFilter, @@ -66,7 +66,12 @@ const usePublisherOptions = (): PublisherProperties | null => { }; setOptions(); - }, [config.videoSettings.resolution, user.defaultSettings]); + }, [ + config.audioSettings.audioOnJoin, + config.videoSettings.resolution, + config.videoSettings.videoOnJoin, + user.defaultSettings, + ]); return publisherOptions; }; From 3269dca579fce5a8bed49ca334350cc624951ccb Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:59:22 -0700 Subject: [PATCH 24/58] background publisher taken care of --- .../useBackgroundPublisher.spec.tsx | 31 ++++++++++++++++++- .../useBackgroundPublisher.tsx | 12 +++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.spec.tsx b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.spec.tsx index 66a4b232..8454081f 100644 --- a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.spec.tsx +++ b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.spec.tsx @@ -1,6 +1,11 @@ import { act, cleanup, renderHook } from '@testing-library/react'; import { afterAll, afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; -import { hasMediaProcessorSupport, initPublisher, Publisher } from '@vonage/client-sdk-video'; +import { + hasMediaProcessorSupport, + initPublisher, + Publisher, + PublisherProperties, +} from '@vonage/client-sdk-video'; import EventEmitter from 'events'; import useBackgroundPublisher from './useBackgroundPublisher'; import { UserContextType } from '../../user'; @@ -14,17 +19,20 @@ import { defaultVideoDevice, } from '../../../utils/mockData/device'; import { DEVICE_ACCESS_STATUS } from '../../../utils/constants'; +import usePublisherOptions from '../../PublisherProvider/usePublisherOptions'; vi.mock('@vonage/client-sdk-video'); vi.mock('../../../hooks/useUserContext.tsx'); vi.mock('../../../hooks/usePermissions.tsx'); vi.mock('../../../hooks/useDevices.tsx'); +vi.mock('../../PublisherProvider/usePublisherOptions'); const mockUseUserContext = useUserContext as Mock<[], UserContextType>; const mockUsePermissions = usePermissions as Mock<[], PermissionsHookType>; const mockUseDevices = useDevices as Mock< [], { allMediaDevices: AllMediaDevices; getAllMediaDevices: () => void } >; +const mockUsePublisherOptions = usePublisherOptions as Mock<[], PublisherProperties>; const defaultSettings = { publishAudio: false, @@ -66,6 +74,9 @@ describe('useBackgroundPublisher', () => { accessStatus: DEVICE_ACCESS_STATUS.PENDING, setAccessStatus: mockSetAccessStatus, }); + mockUsePublisherOptions.mockReturnValue({ + publishVideo: true, + }); }); afterEach(() => { @@ -124,6 +135,24 @@ describe('useBackgroundPublisher', () => { expect.any(Function) ); }); + + it('should initialize with video disabled if configured to be disabled', async () => { + mockedHasMediaProcessorSupport.mockReturnValue(true); + mockedInitPublisher.mockReturnValue(mockPublisher); + mockUsePublisherOptions.mockReturnValue({ + publishVideo: false, + }); + const { result } = renderHook(() => useBackgroundPublisher()); + await result.current.initBackgroundLocalPublisher(); + + expect(mockedInitPublisher).toHaveBeenCalledWith( + undefined, + expect.objectContaining({ + publishVideo: false, + }), + expect.any(Function) + ); + }); }); describe('changeBackground', () => { diff --git a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx index fb87a544..750ade76 100644 --- a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx +++ b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx @@ -15,6 +15,7 @@ import { DEVICE_ACCESS_STATUS } from '../../../utils/constants'; import { AccessDeniedEvent } from '../../PublisherProvider/usePublisher/usePublisher'; import DeviceStore from '../../../utils/DeviceStore'; import applyBackgroundFilter from '../../../utils/backgroundFilter/applyBackgroundFilter/applyBackgroundFilter'; +import usePublisherOptions from '../../PublisherProvider/usePublisherOptions'; export type BackgroundPublisherContextType = { isPublishing: boolean; @@ -54,6 +55,7 @@ type PublisherVideoElementCreatedEvent = Event<'videoElementCreated', Publisher> */ const useBackgroundPublisher = (): BackgroundPublisherContextType => { const { user } = useUserContext(); + const defaultPublisherOptions = usePublisherOptions(); const { allMediaDevices, getAllMediaDevices } = useDevices(); const [publisherVideoElement, setPublisherVideoElement] = useState< HTMLVideoElement | HTMLObjectElement @@ -67,7 +69,9 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { const [backgroundFilter, setBackgroundFilter] = useState( user.defaultSettings.backgroundFilter ); - const [isVideoEnabled, setIsVideoEnabled] = useState(true); + const [isVideoEnabled, setIsVideoEnabled] = useState( + defaultPublisherOptions?.publishVideo ?? false + ); const [localVideoSource, setLocalVideoSource] = useState(undefined); const deviceStoreRef = useRef(new DeviceStore()); @@ -162,7 +166,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { ); const initBackgroundLocalPublisher = useCallback(async () => { - if (backgroundPublisherRef.current) { + if (backgroundPublisherRef.current || !defaultPublisherOptions) { return; } @@ -180,6 +184,8 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { videoFilter, resolution: '1280x720', videoSource, + publishAudio: false, + publishVideo: defaultPublisherOptions?.publishVideo, }; backgroundPublisherRef.current = initPublisher(undefined, publisherOptions, (err: unknown) => { @@ -191,7 +197,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { } }); addPublisherListeners(backgroundPublisherRef.current); - }, [addPublisherListeners]); + }, [addPublisherListeners, defaultPublisherOptions]); /** * Destroys the background publisher From 08891430f52f483ae44b820bc518437b20534241 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:12:20 -0700 Subject: [PATCH 25/58] configured control panel --- .../ConfigProvider/useConfig/useConfig.tsx | 8 ++ .../ControlPanel/ControlPanel.spec.tsx | 32 +++++ .../WaitingRoom/ControlPanel/ControlPanel.tsx | 128 +++++++++--------- 3 files changed, 106 insertions(+), 62 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 534e7324..d625ead8 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -14,9 +14,14 @@ export type AudioSettings = { enableDisableCapableMicrophone: boolean; }; +export type WaitingRoomSettings = { + allowDeviceSelection: boolean; +}; + export type AppConfig = { videoSettings: VideoSettings; audioSettings: AudioSettings; + waitingRoomSettings: WaitingRoomSettings; layoutMode: LayoutMode; }; @@ -32,6 +37,9 @@ export const defaultConfig: AppConfig = { audioOnJoin: true, enableDisableCapableMicrophone: true, }, + waitingRoomSettings: { + allowDeviceSelection: true, + }, layoutMode: 'active-speaker', }; diff --git a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx index 82468b1a..f74b79a5 100644 --- a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx +++ b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.spec.tsx @@ -4,12 +4,16 @@ import ControlPanel from '.'; import useDevices from '../../../hooks/useDevices'; import { AllMediaDevices } from '../../../types'; import { allMediaDevices } from '../../../utils/mockData/device'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/useDevices.tsx'); +vi.mock('../../../hooks/useConfigContext'); const mockUseDevices = useDevices as Mock< [], { allMediaDevices: AllMediaDevices; getAllMediaDevices: () => void } >; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('ControlPanel', () => { beforeEach(() => { @@ -17,6 +21,11 @@ describe('ControlPanel', () => { getAllMediaDevices: vi.fn(), allMediaDevices, }); + mockUseConfigContext.mockReturnValue({ + waitingRoomSettings: { + allowDeviceSelection: true, + }, + } as unknown as ConfigContextType); }); afterEach(() => { @@ -130,4 +139,27 @@ describe('ControlPanel', () => { ); expect(screen.getByTestId('audioOutput-menu')).toBeVisible(); }); + + it('should not be rendered if configured to be hidden', () => { + mockUseConfigContext.mockReturnValue({ + waitingRoomSettings: { + allowDeviceSelection: false, + }, + } as unknown as ConfigContextType); + + render( + {}} + handleVideoInputOpen={() => {}} + handleAudioOutputOpen={() => {}} + handleClose={() => {}} + openAudioInput={false} + openVideoInput={false} + openAudioOutput={false} + anchorEl={null} + /> + ); + + expect(screen.queryByTestId('ControlPanel')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx index c1eff357..028a0f16 100644 --- a/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx +++ b/frontend/src/components/WaitingRoom/ControlPanel/ControlPanel.tsx @@ -9,6 +9,7 @@ import usePreviewPublisherContext from '../../../hooks/usePreviewPublisherContex import useDevices from '../../../hooks/useDevices'; import useAudioOutputContext from '../../../hooks/useAudioOutputContext'; import useIsSmallViewport from '../../../hooks/useIsSmallViewport'; +import useConfigContext from '../../../hooks/useConfigContext'; export type ControlPanelProps = { handleAudioInputOpen: ( @@ -51,12 +52,13 @@ const ControlPanel = ({ openAudioInput, openAudioOutput, anchorEl, -}: ControlPanelProps): ReactElement => { +}: ControlPanelProps): ReactElement | false => { const isSmallViewport = useIsSmallViewport(); const { allMediaDevices } = useDevices(); const { localAudioSource, localVideoSource, changeAudioSource, changeVideoSource } = usePreviewPublisherContext(); const { currentAudioOutputDevice, setAudioOutputDevice } = useAudioOutputContext(); + const { waitingRoomSettings } = useConfigContext(); const buttonSx: SxProps = { borderRadius: '10px', @@ -71,69 +73,71 @@ const ControlPanel = ({ }; return ( -
-
- - - + waitingRoomSettings.allowDeviceSelection && ( +
+
+ + + - - - + + + +
-
+ ) ); }; From f4c15abcf9c91b37c5daa86c2d7dd3a38d5e66fd Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:23:53 -0700 Subject: [PATCH 26/58] fix that test --- .../useConfig/useConfig.spec.tsx | 71 +++++++++++-------- .../ConfigProvider/useConfig/useConfig.tsx | 2 + 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index 9003bef8..0e385608 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import useConfig from './useConfig'; @@ -9,6 +9,12 @@ describe('useConfig', () => { nativeFetch = global.fetch; }); + beforeEach(() => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({}), + }); + }); + afterEach(() => { vi.resetAllMocks(); }); @@ -17,21 +23,28 @@ describe('useConfig', () => { global.fetch = nativeFetch; }); - it('returns the default config when no config.json is loaded', () => { + it('returns the default config when no config.json is loaded', async () => { const { result } = renderHook(() => useConfig()); - expect(result.current).toMatchObject({ - videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, - }, - audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, - }, - layoutMode: 'active-speaker', + + await waitFor(() => { + expect(result.current).toEqual({ + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1280x720', + videoOnJoin: true, + backgroundEffects: true, + }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + enableDisableCapableMicrophone: true, + }, + waitingRoomSettings: { + allowDeviceSelection: true, + enableWaitingRoom: true, + }, + layoutMode: 'active-speaker', + }); }); }); @@ -64,19 +77,21 @@ describe('useConfig', () => { global.fetch = vi.fn().mockRejectedValue(new Error('mocking a failure to fetch')); const { result } = renderHook(() => useConfig()); - expect(result.current).toMatchObject({ - videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, - }, - audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, - }, - layoutMode: 'active-speaker', + await waitFor(() => { + expect(result.current).toMatchObject({ + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1280x720', + videoOnJoin: true, + backgroundEffects: true, + }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + enableDisableCapableMicrophone: true, + }, + layoutMode: 'active-speaker', + }); }); }); }); diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index d625ead8..390b1572 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -16,6 +16,7 @@ export type AudioSettings = { export type WaitingRoomSettings = { allowDeviceSelection: boolean; + enableWaitingRoom: boolean; }; export type AppConfig = { @@ -39,6 +40,7 @@ export const defaultConfig: AppConfig = { }, waitingRoomSettings: { allowDeviceSelection: true, + enableWaitingRoom: true, }, layoutMode: 'active-speaker', }; From 587471ca2282d774b9d2d7dc6c7b518bc81d96a1 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:24:28 -0700 Subject: [PATCH 27/58] too excited --- .../src/Context/ConfigProvider/useConfig/useConfig.spec.tsx | 1 - frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index 0e385608..caeca838 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -41,7 +41,6 @@ describe('useConfig', () => { }, waitingRoomSettings: { allowDeviceSelection: true, - enableWaitingRoom: true, }, layoutMode: 'active-speaker', }); diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 390b1572..d625ead8 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -16,7 +16,6 @@ export type AudioSettings = { export type WaitingRoomSettings = { allowDeviceSelection: boolean; - enableWaitingRoom: boolean; }; export type AppConfig = { @@ -40,7 +39,6 @@ export const defaultConfig: AppConfig = { }, waitingRoomSettings: { allowDeviceSelection: true, - enableWaitingRoom: true, }, layoutMode: 'active-speaker', }; From 61707885de368b57009f76ad2c0996209006080b Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:21:27 -0700 Subject: [PATCH 28/58] participant-list --- .../useConfig/useConfig.spec.tsx | 14 ++++- .../ConfigProvider/useConfig/useConfig.tsx | 24 +++++++- .../src/Context/SessionProvider/session.tsx | 2 +- .../ParticipantListButton.tsx | 57 ++++++++++--------- .../ParticipantsListButton.spec.tsx | 25 +++++++- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index caeca838..4f48be14 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -42,7 +42,10 @@ describe('useConfig', () => { waitingRoomSettings: { allowDeviceSelection: true, }, - layoutMode: 'active-speaker', + meetingRoomSettings: { + layoutMode: 'active-speaker', + showParticipantList: true, + }, }); }); }); @@ -60,7 +63,13 @@ describe('useConfig', () => { audioOnJoin: false, enableDisableCapableMicrophone: false, }, - layoutMode: 'grid', + waitingRoomSettings: { + allowDeviceSelection: false, + }, + meetingRoomSettings: { + layoutMode: 'grid', + showParticipantList: false, + }, }; global.fetch = vi.fn().mockResolvedValue({ json: async () => mockConfig, @@ -89,7 +98,6 @@ describe('useConfig', () => { audioOnJoin: true, enableDisableCapableMicrophone: true, }, - layoutMode: 'active-speaker', }); }); }); diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index d625ead8..54fa33c4 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -18,11 +18,16 @@ export type WaitingRoomSettings = { allowDeviceSelection: boolean; }; +export type MeetingRoomSettings = { + layoutMode: LayoutMode; + showParticipantList: boolean; +}; + export type AppConfig = { videoSettings: VideoSettings; audioSettings: AudioSettings; waitingRoomSettings: WaitingRoomSettings; - layoutMode: LayoutMode; + meetingRoomSettings: MeetingRoomSettings; }; export const defaultConfig: AppConfig = { @@ -40,7 +45,10 @@ export const defaultConfig: AppConfig = { waitingRoomSettings: { allowDeviceSelection: true, }, - layoutMode: 'active-speaker', + meetingRoomSettings: { + layoutMode: 'active-speaker', + showParticipantList: true, + }, }; /** @@ -77,6 +85,18 @@ const useConfig = (): AppConfig => { ...defaultConfig.videoSettings, ...(typedConfigFile.videoSettings || {}), }, + audioSettings: { + ...defaultConfig.audioSettings, + ...(typedConfigFile.audioSettings || {}), + }, + waitingRoomSettings: { + ...defaultConfig.waitingRoomSettings, + ...(typedConfigFile.waitingRoomSettings || {}), + }, + meetingRoomSettings: { + ...defaultConfig.meetingRoomSettings, + ...(typedConfigFile.meetingRoomSettings || {}), + }, }; }, [config]); diff --git a/frontend/src/Context/SessionProvider/session.tsx b/frontend/src/Context/SessionProvider/session.tsx index 74dcd509..9726c386 100644 --- a/frontend/src/Context/SessionProvider/session.tsx +++ b/frontend/src/Context/SessionProvider/session.tsx @@ -134,7 +134,7 @@ const SessionProvider = ({ children }: SessionProviderProps): ReactElement => { const [reconnecting, setReconnecting] = useState(false); const [subscriberWrappers, setSubscriberWrappers] = useState([]); const [ownCaptions, setOwnCaptions] = useState(null); - const [layoutMode, setLayoutMode] = useState(config.layoutMode); + const [layoutMode, setLayoutMode] = useState(config.meetingRoomSettings.layoutMode); const [archiveId, setArchiveId] = useState(null); const activeSpeakerTracker = useRef(new ActiveSpeakerTracker()); const [activeSpeakerId, setActiveSpeakerId] = useState(); diff --git a/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantListButton.tsx b/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantListButton.tsx index a4176de5..daf4e045 100644 --- a/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantListButton.tsx +++ b/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantListButton.tsx @@ -4,6 +4,7 @@ import { blue } from '@mui/material/colors'; import { Badge } from '@mui/material'; import { ReactElement } from 'react'; import ToolbarButton from '../ToolbarButton'; +import useConfigContext from '../../../hooks/useConfigContext'; export type ParticipantListButtonProps = { handleClick: () => void; @@ -28,36 +29,40 @@ const ParticipantListButton = ({ isOpen, participantCount, isOverflowButton = false, -}: ParticipantListButtonProps): ReactElement => { +}: ParticipantListButtonProps): ReactElement | false => { + const { meetingRoomSettings } = useConfigContext(); + return ( - - - } - isOverflowButton={isOverflowButton} - /> - - + overlap="circular" + > + } + isOverflowButton={isOverflowButton} + /> + + + ) ); }; diff --git a/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantsListButton.spec.tsx b/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantsListButton.spec.tsx index 8ff47c55..9b92b7f9 100644 --- a/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantsListButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/ParticipantListButton/ParticipantsListButton.spec.tsx @@ -1,8 +1,22 @@ import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, Mock, vi, beforeEach } from 'vitest'; import ParticipantListButton from './ParticipantListButton'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; + +vi.mock('../../../hooks/useConfigContext'); + +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('ParticipantListButton', () => { + beforeEach(() => { + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showParticipantList: true, + }, + } as unknown as ConfigContextType); + }); + it('should show participant number', () => { render( {}} isOpen={false} participantCount={10} />); expect(screen.getByText('10')).toBeVisible(); @@ -21,4 +35,13 @@ describe('ParticipantListButton', () => { screen.getByRole('button').click(); expect(handleClick).toHaveBeenCalled(); }); + it('should not render if showParticipantList is false in config', () => { + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showParticipantList: false, + }, + } as unknown as ConfigContextType); + render( {}} isOpen={false} participantCount={10} />); + expect(screen.queryByTestId('participant-list-button')).not.toBeInTheDocument(); + }); }); From 3e10a68608bbf61790fffc71102b2da35c0946dd Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:22:52 -0700 Subject: [PATCH 29/58] lint --- frontend/src/Context/ConfigProvider/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/Context/ConfigProvider/index.tsx b/frontend/src/Context/ConfigProvider/index.tsx index 7e332e15..7bbeb424 100644 --- a/frontend/src/Context/ConfigProvider/index.tsx +++ b/frontend/src/Context/ConfigProvider/index.tsx @@ -8,9 +8,10 @@ export type ConfigProviderProps = { export type ConfigContextType = ReturnType; export const ConfigContext = createContext({ - videoSettings: defaultConfig.videoSettings, - layoutMode: defaultConfig.layoutMode, audioSettings: defaultConfig.audioSettings, + meetingRoomSettings: defaultConfig.meetingRoomSettings, + waitingRoomSettings: defaultConfig.waitingRoomSettings, + videoSettings: defaultConfig.videoSettings, }); export const ConfigContextProvider = ({ children }: ConfigProviderProps) => { From 01713d731f83ec0bf4ff8f2b5d4f3149964ef94a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:56:08 -0700 Subject: [PATCH 30/58] config file --- config.example.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/config.example.json b/config.example.json index 2421e7e9..4f35211a 100644 --- a/config.example.json +++ b/config.example.json @@ -1,9 +1,20 @@ { "videoSettings": { + "backgroundEffects": true, "enableDisableCapableCamera": true, "resolution": "1280x720", - "videoOnJoin": true, - "backgroundEffects": true + "videoOnJoin": true }, - "layoutMode": "active-speaker" + "audioSettings": { + "advancedNoiseSuppression": true, + "audioOnJoin": true, + "enableDisableCapableMicrophone": true + }, + "waitingRoomSettings": { + "allowDeviceSelection": true + }, + "meetingRoomSettings": { + "layoutMode": "active-speaker", + "showParticipantList": true + } } From 84e7ec3468702f9234100414317474bea28fe084 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:14:49 -0700 Subject: [PATCH 31/58] resolve another bug --- .../useBackgroundPublisher/useBackgroundPublisher.tsx | 6 +++--- .../src/components/MeetingRoom/Toolbar/Toolbar.tsx | 5 ++++- frontend/src/pages/MeetingRoom/MeetingRoom.tsx | 10 +--------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx index 750ade76..69e7adbd 100644 --- a/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx +++ b/frontend/src/Context/BackgroundPublisherProvider/useBackgroundPublisher/useBackgroundPublisher.tsx @@ -166,7 +166,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { ); const initBackgroundLocalPublisher = useCallback(async () => { - if (backgroundPublisherRef.current || !defaultPublisherOptions) { + if (backgroundPublisherRef.current) { return; } @@ -185,7 +185,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { resolution: '1280x720', videoSource, publishAudio: false, - publishVideo: defaultPublisherOptions?.publishVideo, + publishVideo: isVideoEnabled, }; backgroundPublisherRef.current = initPublisher(undefined, publisherOptions, (err: unknown) => { @@ -197,7 +197,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => { } }); addPublisherListeners(backgroundPublisherRef.current); - }, [addPublisherListeners, defaultPublisherOptions]); + }, [addPublisherListeners, isVideoEnabled]); /** * Destroys the background publisher diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx index 1afdd407..cf844a35 100644 --- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx +++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx @@ -15,6 +15,7 @@ import EmojiGridButton from '../EmojiGridButton'; import isReportIssueEnabled from '../../../utils/isReportIssueEnabled'; import useToolbarButtons from '../../../hooks/useToolbarButtons'; import DeviceControlButton from '../DeviceControlButton'; +import useBackgroundPublisherContext from '../../../hooks/useBackgroundPublisherContext'; export type CaptionsState = { isUserCaptionsEnabled: boolean; @@ -69,6 +70,7 @@ const Toolbar = ({ captionsState, }: ToolbarProps): ReactElement => { const { disconnect, subscriberWrappers } = useSessionContext(); + const { destroyBackgroundPublisher } = useBackgroundPublisherContext(); const isViewingScreenShare = subscriberWrappers.some((subWrapper) => subWrapper.isScreenshare); const isScreenSharePresent = isViewingScreenShare || isSharingScreen; const isPinningPresent = subscriberWrappers.some((subWrapper) => subWrapper.isPinned); @@ -77,7 +79,8 @@ const Toolbar = ({ return; } disconnect(); - }, [disconnect]); + destroyBackgroundPublisher(); + }, [destroyBackgroundPublisher, disconnect]); const [openEmojiGridDesktop, setOpenEmojiGridDesktop] = useState(false); // An array of buttons available for the toolbar. As the toolbar resizes, buttons may be hidden and moved to the diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.tsx index b23da15d..968440c4 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.tsx @@ -38,7 +38,6 @@ const MeetingRoom = (): ReactElement => { initBackgroundLocalPublisher, publisher: backgroundPublisher, accessStatus, - destroyBackgroundPublisher, } = useBackgroundPublisherContext(); const { @@ -99,14 +98,7 @@ const MeetingRoom = (): ReactElement => { if (!backgroundPublisher) { initBackgroundLocalPublisher(); } - - return () => { - // Ensure we destroy the backgroundPublisher and release any media devices. - if (backgroundPublisher) { - destroyBackgroundPublisher(); - } - }; - }, [initBackgroundLocalPublisher, backgroundPublisher, destroyBackgroundPublisher]); + }, [initBackgroundLocalPublisher, backgroundPublisher]); // After changing device permissions, reload the page to reflect the device's permission change. useEffect(() => { From f5fa301691c9cdca5234384fc4f501478e22b190 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:10:46 -0700 Subject: [PATCH 32/58] fixed tests --- frontend/src/Context/SessionProvider/session.spec.tsx | 9 +++++++++ frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx | 3 +++ 2 files changed, 12 insertions(+) diff --git a/frontend/src/Context/SessionProvider/session.spec.tsx b/frontend/src/Context/SessionProvider/session.spec.tsx index 68725136..0018de4c 100644 --- a/frontend/src/Context/SessionProvider/session.spec.tsx +++ b/frontend/src/Context/SessionProvider/session.spec.tsx @@ -20,6 +20,15 @@ vi.mock('../../utils/constants', () => ({ MAX_PIN_COUNT_DESKTOP: 1, })); vi.mock('../../api/fetchCredentials'); +vi.mock('../../hooks/useConfigContext', () => { + return { + default: () => ({ + meetingRoomSettings: { + layoutMode: 'active-speaker', + }, + }), + }; +}); const mockFetchCredentials = fetchCredentials as Mock; diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx index bf1781c5..7fdd1c44 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx @@ -63,6 +63,9 @@ vi.mock('../../hooks/useConfigContext', () => { audioSettings: { enableDisableCapableMicrophone: true, }, + meetingRoomSettings: { + layoutMode: 'active-speaker', + }, }), }; }); From 4d478d5f4b1a7f3c659958dee9bac57ab409f3f1 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:58:41 -0700 Subject: [PATCH 33/58] chat button --- config.example.json | 3 +- .../useConfig/useConfig.spec.tsx | 61 ++++++++----------- .../ConfigProvider/useConfig/useConfig.tsx | 2 + .../ChatButton/ChatButton.spec.tsx | 20 ++++++ .../MeetingRoom/ChatButton/ChatButton.tsx | 37 ++++++----- .../UnreadMessagesBadge.spec.tsx | 34 +++++++++++ .../UnreadMessagesBadge.tsx | 6 +- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 1 + 8 files changed, 111 insertions(+), 53 deletions(-) diff --git a/config.example.json b/config.example.json index 4f35211a..3aa6c64f 100644 --- a/config.example.json +++ b/config.example.json @@ -15,6 +15,7 @@ }, "meetingRoomSettings": { "layoutMode": "active-speaker", - "showParticipantList": true + "showParticipantList": true, + "showChat": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index 4f48be14..a46f3299 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -1,9 +1,30 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; -import useConfig from './useConfig'; +import useConfig, { AppConfig } from './useConfig'; describe('useConfig', () => { let nativeFetch: typeof global.fetch; + const defaultConfig: AppConfig = { + videoSettings: { + enableDisableCapableCamera: true, + resolution: '1280x720', + videoOnJoin: true, + backgroundEffects: true, + }, + audioSettings: { + advancedNoiseSuppression: true, + audioOnJoin: true, + enableDisableCapableMicrophone: true, + }, + waitingRoomSettings: { + allowDeviceSelection: true, + }, + meetingRoomSettings: { + layoutMode: 'active-speaker', + showParticipantList: true, + showChat: true, + }, + }; beforeAll(() => { nativeFetch = global.fetch; @@ -27,31 +48,12 @@ describe('useConfig', () => { const { result } = renderHook(() => useConfig()); await waitFor(() => { - expect(result.current).toEqual({ - videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, - }, - audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, - }, - waitingRoomSettings: { - allowDeviceSelection: true, - }, - meetingRoomSettings: { - layoutMode: 'active-speaker', - showParticipantList: true, - }, - }); + expect(result.current).toEqual(defaultConfig); }); }); it('merges config.json values if loaded (mocked fetch)', async () => { - const mockConfig = { + const mockConfig: AppConfig = { videoSettings: { enableDisableCapableCamera: false, resolution: '640x480', @@ -69,6 +71,7 @@ describe('useConfig', () => { meetingRoomSettings: { layoutMode: 'grid', showParticipantList: false, + showChat: false, }, }; global.fetch = vi.fn().mockResolvedValue({ @@ -86,19 +89,7 @@ describe('useConfig', () => { const { result } = renderHook(() => useConfig()); await waitFor(() => { - expect(result.current).toMatchObject({ - videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, - }, - audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, - }, - }); + expect(result.current).toEqual(defaultConfig); }); }); }); diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 54fa33c4..87873f0b 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -21,6 +21,7 @@ export type WaitingRoomSettings = { export type MeetingRoomSettings = { layoutMode: LayoutMode; showParticipantList: boolean; + showChat: boolean; }; export type AppConfig = { @@ -48,6 +49,7 @@ export const defaultConfig: AppConfig = { meetingRoomSettings: { layoutMode: 'active-speaker', showParticipantList: true, + showChat: true, }, }; diff --git a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx index f18f11c1..fbcd8ea3 100644 --- a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx @@ -3,16 +3,26 @@ import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import ChatButton from './ChatButton'; import useSessionContext from '../../../hooks/useSessionContext'; import { SessionContextType } from '../../../Context/SessionProvider/session'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/useSessionContext'); +vi.mock('../../../hooks/useConfigContext'); const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>; const sessionContext = { unreadCount: 10, } as unknown as SessionContextType; +const mockConfigContext = { + meetingRoomSettings: { + showChat: true, + }, +} as unknown as ConfigContextType; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('ChatButton', () => { beforeEach(() => { mockUseSessionContext.mockReturnValue(sessionContext); + mockUseConfigContext.mockReturnValue(mockConfigContext); }); it('should show unread message number', () => { @@ -51,4 +61,14 @@ describe('ChatButton', () => { screen.getByRole('button').click(); expect(handleClick).toHaveBeenCalled(); }); + + it('should not be displayed when chat is disabled', () => { + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showChat: false, + }, + } as unknown as ConfigContextType); + render( {}} isOpen />); + expect(screen.queryByTestId('ChatIcon')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx index fcb34722..d2feeea7 100644 --- a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx +++ b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx @@ -4,6 +4,7 @@ import { blue } from '@mui/material/colors'; import { ReactElement } from 'react'; import ToolbarButton from '../ToolbarButton'; import UnreadMessagesBadge from '../UnreadMessagesBadge'; +import useConfigContext from '../../../hooks/useConfigContext'; export type ChatButtonProps = { handleClick: () => void; @@ -20,28 +21,32 @@ export type ChatButtonProps = { * @property {() => void} handleClick - click handler to toggle open chat panel * @property {boolean} isOpen - true if chat is currently open, false if not * @property {boolean} isOverflowButton - (optional) whether the button is in the ToolbarOverflowMenu - * @returns {ReactElement} - ChatButton + * @returns {ReactElement | false} - ChatButton */ const ChatButton = ({ handleClick, isOpen, isOverflowButton = false, -}: ChatButtonProps): ReactElement => { +}: ChatButtonProps): ReactElement | false => { + const { meetingRoomSettings } = useConfigContext(); + return ( - - - } - isOverflowButton={isOverflowButton} - /> - - + meetingRoomSettings.showChat && ( + + + } + isOverflowButton={isOverflowButton} + /> + + + ) ); }; diff --git a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx index 116882f9..c14360a9 100644 --- a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx +++ b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx @@ -5,17 +5,27 @@ import useSessionContext from '../../../hooks/useSessionContext'; import { SessionContextType } from '../../../Context/SessionProvider/session'; import UnreadMessagesBadge from './UnreadMessagesBadge'; import ToolbarButton from '../ToolbarButton'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/useSessionContext'); +vi.mock('../../../hooks/useConfigContext'); const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>; const sessionContext = { unreadCount: 0, } as unknown as SessionContextType; const LittleButton = () => {}} icon={} />; +const mockConfigContext = { + meetingRoomSettings: { + showChat: true, + }, +} as unknown as ConfigContextType; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('UnreadMessagesBadge', () => { beforeEach(() => { mockUseSessionContext.mockReturnValue(sessionContext); + mockUseConfigContext.mockReturnValue(mockConfigContext); }); it('shows badge with correct unread message count', () => { @@ -160,4 +170,28 @@ describe('UnreadMessagesBadge', () => { expect(updatedBadge).toBeVisible(); expect(updatedBadge.textContent).toBe('1'); }); + + it('should not show the message badge when it is configured to be hidden', () => { + const sessionContextWithMessages: SessionContextType = { + ...sessionContext, + unreadCount: 8, + } as unknown as SessionContextType; + mockUseSessionContext.mockReturnValue(sessionContextWithMessages); + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showChat: false, + }, + } as unknown as ConfigContextType); + + render( + + + + ); + + const badge = screen.getByTestId('chat-button-unread-count'); + // Check badge is hidden: MUI hides badge by setting dimensions to 0x0 + expect(badge.offsetHeight).toBe(0); + expect(badge.offsetWidth).toBe(0); + }); }); diff --git a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx index 494e832c..62b6a642 100644 --- a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx +++ b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx @@ -1,6 +1,7 @@ import { Badge } from '@mui/material'; import { ForwardedRef, forwardRef, ReactElement } from 'react'; import useSessionContext from '../../../hooks/useSessionContext'; +import useConfigContext from '../../../hooks/useConfigContext'; export type UnreadMessagesBadgeProps = { children: ReactElement; @@ -20,9 +21,12 @@ const UnreadMessagesBadge = forwardRef(function UnreadMessagesBadge( props: UnreadMessagesBadgeProps, ref: ForwardedRef ) { + const { meetingRoomSettings } = useConfigContext(); const { children, isToolbarOverflowMenuOpen, ...rest } = props; const { unreadCount } = useSessionContext(); - const isInvisible = unreadCount === 0 || isToolbarOverflowMenuOpen; + const isInvisible = + unreadCount === 0 || isToolbarOverflowMenuOpen || !meetingRoomSettings.showChat; + return ( { }, meetingRoomSettings: { layoutMode: 'active-speaker', + showChat: true, }, }), }; From f37c7a862952b0cb68e770efe8458a6a2c844c82 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:02:20 -0700 Subject: [PATCH 34/58] screen-sharing --- config.example.json | 3 ++- .../useConfig/useConfig.spec.tsx | 2 ++ .../ConfigProvider/useConfig/useConfig.tsx | 2 ++ .../ScreenSharingButton.spec.tsx | 23 ++++++++++++++++++- .../ScreenSharingButton.tsx | 5 +++- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 1 + 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/config.example.json b/config.example.json index 3aa6c64f..ee253e74 100644 --- a/config.example.json +++ b/config.example.json @@ -16,6 +16,7 @@ "meetingRoomSettings": { "layoutMode": "active-speaker", "showParticipantList": true, - "showChat": true + "showChat": true, + "showScreenShareButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index a46f3299..f95b99e3 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -23,6 +23,7 @@ describe('useConfig', () => { layoutMode: 'active-speaker', showParticipantList: true, showChat: true, + showScreenShareButton: true, }, }; @@ -72,6 +73,7 @@ describe('useConfig', () => { layoutMode: 'grid', showParticipantList: false, showChat: false, + showScreenShareButton: false, }, }; global.fetch = vi.fn().mockResolvedValue({ diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 87873f0b..69e523b9 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -22,6 +22,7 @@ export type MeetingRoomSettings = { layoutMode: LayoutMode; showParticipantList: boolean; showChat: boolean; + showScreenShareButton: boolean; }; export type AppConfig = { @@ -50,6 +51,7 @@ export const defaultConfig: AppConfig = { layoutMode: 'active-speaker', showParticipantList: true, showChat: true, + showScreenShareButton: true, }, }; diff --git a/frontend/src/components/ScreenSharingButton/ScreenSharingButton.spec.tsx b/frontend/src/components/ScreenSharingButton/ScreenSharingButton.spec.tsx index 70c948f6..f9ad6502 100644 --- a/frontend/src/components/ScreenSharingButton/ScreenSharingButton.spec.tsx +++ b/frontend/src/components/ScreenSharingButton/ScreenSharingButton.spec.tsx @@ -1,8 +1,23 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import ScreenSharingButton, { ScreenShareButtonProps } from './ScreenSharingButton'; +import useConfigContext from '../../hooks/useConfigContext'; +import { ConfigContextType } from '../../Context/ConfigProvider'; + +vi.mock('../../hooks/useConfigContext'); + +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; +const mockConfigContext = { + meetingRoomSettings: { + showScreenShareButton: true, + }, +} as unknown as ConfigContextType; describe('ScreenSharingButton', () => { + beforeEach(() => { + mockUseConfigContext.mockReturnValue(mockConfigContext); + }); + const mockToggleScreenShare = vi.fn(); const defaultProps: ScreenShareButtonProps = { @@ -41,4 +56,10 @@ describe('ScreenSharingButton', () => { ) ).toBeInTheDocument(); }); + + it('does not show the button when the button is configured to be hidden', () => { + mockConfigContext.meetingRoomSettings.showScreenShareButton = false; + render(); + expect(screen.queryByTestId('ScreenShareIcon')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/ScreenSharingButton/ScreenSharingButton.tsx b/frontend/src/components/ScreenSharingButton/ScreenSharingButton.tsx index dfb4a8c3..1aac1444 100644 --- a/frontend/src/components/ScreenSharingButton/ScreenSharingButton.tsx +++ b/frontend/src/components/ScreenSharingButton/ScreenSharingButton.tsx @@ -5,6 +5,7 @@ import { ReactElement, useState } from 'react'; import ToolbarButton from '../MeetingRoom/ToolbarButton'; import PopupDialog, { DialogTexts } from '../MeetingRoom/PopupDialog'; import { isMobile } from '../../utils/util'; +import useConfigContext from '../../hooks/useConfigContext'; export type ScreenShareButtonProps = { toggleScreenShare: () => void; @@ -30,6 +31,7 @@ const ScreenSharingButton = ({ isViewingScreenShare, isOverflowButton = false, }: ScreenShareButtonProps): ReactElement | false => { + const { meetingRoomSettings } = useConfigContext(); const title = isSharingScreen ? 'Stop screen share' : 'Start screen share'; const [isModalOpen, setIsModalOpen] = useState(false); @@ -56,7 +58,8 @@ const ScreenSharingButton = ({ return ( // Screensharing relies on the getDisplayMedia browser API which is unsupported on mobile devices // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#browser_compatibility - !isMobile() && ( + !isMobile() && + meetingRoomSettings.showScreenShareButton && ( <> { meetingRoomSettings: { layoutMode: 'active-speaker', showChat: true, + showScreenShareButton: true, }, }), }; From 83808773548cdfb54bbe83b33ee6b6f6bb2af56a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:16:36 -0700 Subject: [PATCH 35/58] archiving button. layoutMode --> defaultLayoutMode --- config.example.json | 3 +- .../useConfig/useConfig.spec.tsx | 6 ++- .../ConfigProvider/useConfig/useConfig.tsx | 6 ++- .../Context/SessionProvider/session.spec.tsx | 2 +- .../src/Context/SessionProvider/session.tsx | 4 +- .../ArchivingButton/ArchivingButton.spec.tsx | 22 ++++++++ .../ArchivingButton/ArchivingButton.tsx | 52 ++++++++++--------- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 3 +- 8 files changed, 66 insertions(+), 32 deletions(-) diff --git a/config.example.json b/config.example.json index ee253e74..e2f00318 100644 --- a/config.example.json +++ b/config.example.json @@ -17,6 +17,7 @@ "layoutMode": "active-speaker", "showParticipantList": true, "showChat": true, - "showScreenShareButton": true + "showScreenShareButton": true, + "showArchiveButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index f95b99e3..02988f09 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -20,10 +20,11 @@ describe('useConfig', () => { allowDeviceSelection: true, }, meetingRoomSettings: { - layoutMode: 'active-speaker', + defaultLayoutMode: 'active-speaker', showParticipantList: true, showChat: true, showScreenShareButton: true, + showArchiveButton: true, }, }; @@ -70,10 +71,11 @@ describe('useConfig', () => { allowDeviceSelection: false, }, meetingRoomSettings: { - layoutMode: 'grid', + defaultLayoutMode: 'grid', showParticipantList: false, showChat: false, showScreenShareButton: false, + showArchiveButton: false, }, }; global.fetch = vi.fn().mockResolvedValue({ diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 69e523b9..d85096d8 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -19,10 +19,11 @@ export type WaitingRoomSettings = { }; export type MeetingRoomSettings = { - layoutMode: LayoutMode; + defaultLayoutMode: LayoutMode; showParticipantList: boolean; showChat: boolean; showScreenShareButton: boolean; + showArchiveButton: boolean; }; export type AppConfig = { @@ -48,10 +49,11 @@ export const defaultConfig: AppConfig = { allowDeviceSelection: true, }, meetingRoomSettings: { - layoutMode: 'active-speaker', + defaultLayoutMode: 'active-speaker', showParticipantList: true, showChat: true, showScreenShareButton: true, + showArchiveButton: true, }, }; diff --git a/frontend/src/Context/SessionProvider/session.spec.tsx b/frontend/src/Context/SessionProvider/session.spec.tsx index 0018de4c..51a6ef99 100644 --- a/frontend/src/Context/SessionProvider/session.spec.tsx +++ b/frontend/src/Context/SessionProvider/session.spec.tsx @@ -24,7 +24,7 @@ vi.mock('../../hooks/useConfigContext', () => { return { default: () => ({ meetingRoomSettings: { - layoutMode: 'active-speaker', + defaultLayoutMode: 'active-speaker', }, }), }; diff --git a/frontend/src/Context/SessionProvider/session.tsx b/frontend/src/Context/SessionProvider/session.tsx index 9726c386..0cc7e7cf 100644 --- a/frontend/src/Context/SessionProvider/session.tsx +++ b/frontend/src/Context/SessionProvider/session.tsx @@ -134,7 +134,9 @@ const SessionProvider = ({ children }: SessionProviderProps): ReactElement => { const [reconnecting, setReconnecting] = useState(false); const [subscriberWrappers, setSubscriberWrappers] = useState([]); const [ownCaptions, setOwnCaptions] = useState(null); - const [layoutMode, setLayoutMode] = useState(config.meetingRoomSettings.layoutMode); + const [layoutMode, setLayoutMode] = useState( + config.meetingRoomSettings.defaultLayoutMode + ); const [archiveId, setArchiveId] = useState(null); const activeSpeakerTracker = useRef(new ActiveSpeakerTracker()); const [activeSpeakerId, setActiveSpeakerId] = useState(); diff --git a/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.spec.tsx b/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.spec.tsx index 1a6dea53..7d36758c 100644 --- a/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.spec.tsx @@ -5,9 +5,12 @@ import ArchivingButton from './ArchivingButton'; import useRoomName from '../../../hooks/useRoomName'; import useSessionContext from '../../../hooks/useSessionContext'; import { SessionContextType } from '../../../Context/SessionProvider/session'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/useSessionContext'); vi.mock('../../../hooks/useRoomName'); +vi.mock('../../../hooks/useConfigContext'); vi.mock('../../../api/archiving', () => ({ startArchiving: vi.fn(), @@ -18,8 +21,10 @@ describe('ArchivingButton', () => { const mockHandleCloseMenu = vi.fn(); const mockedRoomName = 'test-room-name'; let sessionContext: SessionContextType; + let configContext: ConfigContextType; const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>; + const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; const testArchiveId = 'test-archive-id'; beforeEach(() => { @@ -29,8 +34,14 @@ describe('ArchivingButton', () => { subscriberWrappers: [], archiveId: null, } as unknown as SessionContextType; + configContext = { + meetingRoomSettings: { + showArchiveButton: true, + }, + } as unknown as ConfigContextType; mockUseSessionContext.mockReturnValue(sessionContext as unknown as SessionContextType); + mockUseConfigContext.mockReturnValue(configContext); }); it('renders the button correctly', () => { @@ -86,4 +97,15 @@ describe('ArchivingButton', () => { expect(stopArchiving).toHaveBeenCalledWith(mockedRoomName, testArchiveId); }); }); + + it('is not displayed when archiving is disabled', () => { + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showArchiveButton: false, + }, + } as unknown as ConfigContextType); + + render(); + expect(screen.queryByTestId('archiving-button')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.tsx b/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.tsx index 09dd07cc..b739f886 100644 --- a/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.tsx +++ b/frontend/src/components/MeetingRoom/ArchivingButton/ArchivingButton.tsx @@ -6,6 +6,7 @@ import ToolbarButton from '../ToolbarButton'; import PopupDialog, { DialogTexts } from '../PopupDialog'; import { startArchiving, stopArchiving } from '../../../api/archiving'; import useSessionContext from '../../../hooks/useSessionContext'; +import useConfigContext from '../../../hooks/useConfigContext'; export type ArchivingButtonProps = { isOverflowButton?: boolean; @@ -21,14 +22,15 @@ export type ArchivingButtonProps = { * @param {ArchivingButtonProps} props - the props for the component * @property {boolean} isOverflowButton - (optional) whether the button is in the ToolbarOverflowMenu * @property {(event?: MouseEvent | TouchEvent) => void} handleClick - (optional) click handler that closes the overflow menu in small viewports. - * @returns {ReactElement} - The ArchivingButton component. + * @returns {ReactElement | false} - The ArchivingButton component. */ const ArchivingButton = ({ isOverflowButton = false, handleClick, -}: ArchivingButtonProps): ReactElement => { +}: ArchivingButtonProps): ReactElement | false => { const roomName = useRoomName(); const { archiveId } = useSessionContext(); + const { meetingRoomSettings } = useConfigContext(); const isRecording = !!archiveId; const [isModalOpen, setIsModalOpen] = useState(false); const title = isRecording ? 'Stop recording' : 'Start recording'; @@ -82,29 +84,31 @@ const ArchivingButton = ({ }; return ( - <> - - - } - sx={{ - marginTop: isOverflowButton ? '0px' : '4px', - }} - isOverflowButton={isOverflowButton} + meetingRoomSettings.showArchiveButton && ( + <> + + + } + sx={{ + marginTop: isOverflowButton ? '0px' : '4px', + }} + isOverflowButton={isOverflowButton} + /> + + - - - + + ) ); }; export default ArchivingButton; diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx index 0ffab32d..b37fa18d 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx @@ -64,9 +64,10 @@ vi.mock('../../hooks/useConfigContext', () => { enableDisableCapableMicrophone: true, }, meetingRoomSettings: { - layoutMode: 'active-speaker', + defaultLayoutMode: 'active-speaker', showChat: true, showScreenShareButton: true, + showArchiveButton: true, }, }), }; From 1c2660d58765203383540a41b3d11e8c3279531a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:22:22 -0700 Subject: [PATCH 36/58] captions button --- config.example.json | 3 +- .../useConfig/useConfig.spec.tsx | 2 + .../ConfigProvider/useConfig/useConfig.tsx | 2 + .../CaptionsButton/CaptionsButton.spec.tsx | 21 ++++++++ .../CaptionsButton/CaptionsButton.tsx | 50 ++++++++++--------- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 1 + 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/config.example.json b/config.example.json index e2f00318..74161875 100644 --- a/config.example.json +++ b/config.example.json @@ -18,6 +18,7 @@ "showParticipantList": true, "showChat": true, "showScreenShareButton": true, - "showArchiveButton": true + "showArchiveButton": true, + "showCaptionsButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index 02988f09..f9f86744 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -25,6 +25,7 @@ describe('useConfig', () => { showChat: true, showScreenShareButton: true, showArchiveButton: true, + showCaptionsButton: true, }, }; @@ -76,6 +77,7 @@ describe('useConfig', () => { showChat: false, showScreenShareButton: false, showArchiveButton: false, + showCaptionsButton: false, }, }; global.fetch = vi.fn().mockResolvedValue({ diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index d85096d8..e27e71d4 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -24,6 +24,7 @@ export type MeetingRoomSettings = { showChat: boolean; showScreenShareButton: boolean; showArchiveButton: boolean; + showCaptionsButton: boolean; }; export type AppConfig = { @@ -54,6 +55,7 @@ export const defaultConfig: AppConfig = { showChat: true, showScreenShareButton: true, showArchiveButton: true, + showCaptionsButton: true, }, }; diff --git a/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.spec.tsx b/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.spec.tsx index 9af29f98..78e1997f 100644 --- a/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.spec.tsx @@ -8,6 +8,8 @@ import useRoomName from '../../../hooks/useRoomName'; import { SessionContextType } from '../../../Context/SessionProvider/session'; import useSessionContext from '../../../hooks/useSessionContext'; import { SubscriberWrapper } from '../../../types/session'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('../../../hooks/useSessionContext'); vi.mock('../../../hooks/useRoomName'); @@ -15,8 +17,10 @@ vi.mock('../../../api/captions', () => ({ enableCaptions: vi.fn(), disableCaptions: vi.fn(), })); +vi.mock('../../../hooks/useConfigContext'); const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>; +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; describe('CaptionsButton', () => { const mockHandleCloseMenu = vi.fn(); @@ -30,6 +34,7 @@ describe('CaptionsButton', () => { } as CaptionsState; const mockedRoomName = 'test-room-name'; let sessionContext: SessionContextType; + let configContext: ConfigContextType; const createSubscriberWrapper = (id: string): SubscriberWrapper => { const mockSubscriber = { @@ -64,7 +69,13 @@ describe('CaptionsButton', () => { sessionContext = { subscriberWrappers: [createSubscriberWrapper('subscriber-1')], } as unknown as SessionContextType; + configContext = { + meetingRoomSettings: { + showCaptionsButton: true, + }, + } as unknown as ConfigContextType; mockUseSessionContext.mockReturnValue(sessionContext as unknown as SessionContextType); + mockUseConfigContext.mockReturnValue(configContext); }); it('renders the button correctly', () => { @@ -85,4 +96,14 @@ describe('CaptionsButton', () => { expect(enableCaptions).toHaveBeenCalledWith(mockedRoomName); }); }); + + it('is not displayed when configured to be disabled', () => { + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showCaptionsButton: false, + }, + } as unknown as ConfigContextType); + render(); + expect(screen.queryByTestId('captions-button')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.tsx b/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.tsx index 16f33624..0c6f8b0c 100644 --- a/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.tsx +++ b/frontend/src/components/MeetingRoom/CaptionsButton/CaptionsButton.tsx @@ -5,6 +5,7 @@ import { AxiosError } from 'axios'; import useRoomName from '../../../hooks/useRoomName'; import ToolbarButton from '../ToolbarButton'; import { disableCaptions, enableCaptions } from '../../../api/captions'; +import useConfigContext from '../../../hooks/useConfigContext'; export type CaptionsState = { isUserCaptionsEnabled: boolean; @@ -26,13 +27,14 @@ export type CaptionsButtonProps = { * @property {boolean} isOverflowButton - (optional) whether the button is in the ToolbarOverflowMenu * @property {(event?: MouseEvent | TouchEvent) => void} handleClick - (optional) click handler that closes the overflow menu in small viewports. * @property {CaptionsState} captionsState - the state of the captions, including whether they are enabled and functions to set error messages - * @returns {ReactElement} - The CaptionsButton component. + * @returns {ReactElement | false} - The CaptionsButton component. */ const CaptionsButton = ({ isOverflowButton = false, handleClick, captionsState, -}: CaptionsButtonProps): ReactElement => { +}: CaptionsButtonProps): ReactElement | false => { + const { meetingRoomSettings } = useConfigContext(); const roomName = useRoomName(); const [captionsId, setCaptionsId] = useState(''); const { isUserCaptionsEnabled, setIsUserCaptionsEnabled, setCaptionsErrorResponse } = @@ -91,27 +93,29 @@ const CaptionsButton = ({ }; return ( - - - ) : ( - - ) - } - sx={{ - marginTop: isOverflowButton ? '0px' : '4px', - }} - isOverflowButton={isOverflowButton} - /> - + meetingRoomSettings.showCaptionsButton && ( + + + ) : ( + + ) + } + sx={{ + marginTop: isOverflowButton ? '0px' : '4px', + }} + isOverflowButton={isOverflowButton} + /> + + ) ); }; export default CaptionsButton; diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx index b37fa18d..bdfc266e 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx @@ -68,6 +68,7 @@ vi.mock('../../hooks/useConfigContext', () => { showChat: true, showScreenShareButton: true, showArchiveButton: true, + showCaptionsButton: true, }, }), }; From 7f6e5c04a9f3237191629fc38b2b5176dc5f81ed Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:32:49 -0700 Subject: [PATCH 37/58] showemoji --- config.example.json | 3 +- .../useConfig/useConfig.spec.tsx | 2 + .../ConfigProvider/useConfig/useConfig.tsx | 2 + .../EmojiGridButton/EmojiGridButton.spec.tsx | 19 +++++++ .../EmojiGridButton/EmojiGridButton.tsx | 56 ++++++++++--------- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 2 + 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/config.example.json b/config.example.json index 74161875..171a6fea 100644 --- a/config.example.json +++ b/config.example.json @@ -19,6 +19,7 @@ "showChat": true, "showScreenShareButton": true, "showArchiveButton": true, - "showCaptionsButton": true + "showCaptionsButton": true, + "showEmojiButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index f9f86744..be086682 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -26,6 +26,7 @@ describe('useConfig', () => { showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, + showEmojiButton: true, }, }; @@ -78,6 +79,7 @@ describe('useConfig', () => { showScreenShareButton: false, showArchiveButton: false, showCaptionsButton: false, + showEmojiButton: false, }, }; global.fetch = vi.fn().mockResolvedValue({ diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index e27e71d4..ba8e195a 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -25,6 +25,7 @@ export type MeetingRoomSettings = { showScreenShareButton: boolean; showArchiveButton: boolean; showCaptionsButton: boolean; + showEmojiButton: boolean; }; export type AppConfig = { @@ -56,6 +57,7 @@ export const defaultConfig: AppConfig = { showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, + showEmojiButton: true, }, }; diff --git a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.spec.tsx b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.spec.tsx index afd36985..6deeee33 100644 --- a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.spec.tsx @@ -3,6 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; import { useState } from 'react'; import * as mui from '@mui/material'; import EmojiGridButton from './EmojiGridButton'; +import useConfigContext from '../../../hooks/useConfigContext'; +import { ConfigContextType } from '../../../Context/ConfigProvider'; vi.mock('@mui/material', async () => { const actual = await vi.importActual('@mui/material'); @@ -14,6 +16,9 @@ vi.mock('@mui/material', async () => { vi.mock('../../../utils/emojis', () => ({ default: { FAVORITE: '🦧' }, })); +vi.mock('../../../hooks/useConfigContext'); + +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; const TestComponent = ({ defaultOpenEmojiGrid = false }: { defaultOpenEmojiGrid?: boolean }) => { const [isEmojiGridOpen, setIsEmojiGridOpen] = useState(defaultOpenEmojiGrid); @@ -27,8 +32,16 @@ const TestComponent = ({ defaultOpenEmojiGrid = false }: { defaultOpenEmojiGrid? }; describe('EmojiGridButton', () => { + let mockConfigContext: ConfigContextType; + beforeEach(() => { (mui.useMediaQuery as Mock).mockReturnValue(false); + mockConfigContext = { + meetingRoomSettings: { + showEmojiButton: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(mockConfigContext); }); afterEach(() => { @@ -52,4 +65,10 @@ describe('EmojiGridButton', () => { rerender(); expect(screen.getByTestId('emoji-grid')).toBeVisible(); }); + + it('is not displayed when configured to be hidden', () => { + mockConfigContext.meetingRoomSettings.showEmojiButton = false; + render(); + expect(screen.queryByTestId('emoji-grid-button')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx index 7ff24378..03046c67 100644 --- a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx +++ b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx @@ -3,6 +3,7 @@ import { EmojiEmotions } from '@mui/icons-material'; import { Dispatch, ReactElement, SetStateAction, useRef } from 'react'; import ToolbarButton from '../ToolbarButton'; import EmojiGrid from '../EmojiGrid/EmojiGrid'; +import useConfigContext from '../../../hooks/useConfigContext'; export type EmojiGridProps = { isEmojiGridOpen: boolean; @@ -20,45 +21,48 @@ export type EmojiGridProps = { * @property {Dispatch>} setIsEmojiGridOpen - toggle whether the emoji grid is shown or hidden * @property {boolean} isParentOpen - whether the ToolbarOverflowMenu is open * @property {boolean} isOverflowButton - (optional) whether the button is in the ToolbarOverflowMenu - * @returns {ReactElement} - The EmojiGridButton Component. + * @returns {ReactElement | false} - The EmojiGridButton Component. */ const EmojiGridButton = ({ isEmojiGridOpen, setIsEmojiGridOpen, isParentOpen, isOverflowButton = false, -}: EmojiGridProps): ReactElement => { +}: EmojiGridProps): ReactElement | false => { + const { meetingRoomSettings } = useConfigContext(); const anchorRef = useRef(null); const handleToggle = () => { setIsEmojiGridOpen((prevOpen) => !prevOpen); }; return ( - <> - - - } - ref={anchorRef} - data-testid="emoji-grid-button" - sx={{ - marginTop: isOverflowButton ? '0px' : '4px', - }} - isOverflowButton={isOverflowButton} - /> - + meetingRoomSettings.showEmojiButton && ( + <> + + + } + ref={anchorRef} + data-testid="emoji-grid-button" + sx={{ + marginTop: isOverflowButton ? '0px' : '4px', + }} + isOverflowButton={isOverflowButton} + /> + - - + + + ) ); }; diff --git a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx index bdfc266e..87a97017 100644 --- a/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx +++ b/frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx @@ -65,10 +65,12 @@ vi.mock('../../hooks/useConfigContext', () => { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', + showParticipantList: true, showChat: true, showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, + showEmojiButton: true, }, }), }; From 70f83690fef86f9ba40884df61638fb30264300d Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:35:23 -0700 Subject: [PATCH 38/58] rename some flags --- config.example.json | 10 +++++----- .../ConfigProvider/useConfig/useConfig.spec.tsx | 8 ++++---- .../src/Context/ConfigProvider/useConfig/useConfig.tsx | 10 +++++----- .../MeetingRoom/ChatButton/ChatButton.spec.tsx | 4 ++-- .../components/MeetingRoom/ChatButton/ChatButton.tsx | 2 +- .../ParticipantListButton/ParticipantListButton.tsx | 2 +- .../ParticipantsListButton.spec.tsx | 6 +++--- .../UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx | 4 ++-- .../UnreadMessagesBadge/UnreadMessagesBadge.tsx | 2 +- frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx | 4 ++-- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/config.example.json b/config.example.json index 171a6fea..8b90467b 100644 --- a/config.example.json +++ b/config.example.json @@ -14,12 +14,12 @@ "allowDeviceSelection": true }, "meetingRoomSettings": { - "layoutMode": "active-speaker", - "showParticipantList": true, - "showChat": true, - "showScreenShareButton": true, + "defaultLayoutMode": "active-speaker", "showArchiveButton": true, "showCaptionsButton": true, - "showEmojiButton": true + "showChatButton": true, + "showEmojiButton": true, + "showParticipantListButton": true, + "showScreenShareButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index be086682..b3d6675f 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -21,8 +21,8 @@ describe('useConfig', () => { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', - showParticipantList: true, - showChat: true, + showParticipantListButton: true, + showChatButton: true, showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, @@ -74,8 +74,8 @@ describe('useConfig', () => { }, meetingRoomSettings: { defaultLayoutMode: 'grid', - showParticipantList: false, - showChat: false, + showParticipantListButton: false, + showChatButton: false, showScreenShareButton: false, showArchiveButton: false, showCaptionsButton: false, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index ba8e195a..3be47b5f 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -20,12 +20,12 @@ export type WaitingRoomSettings = { export type MeetingRoomSettings = { defaultLayoutMode: LayoutMode; - showParticipantList: boolean; - showChat: boolean; - showScreenShareButton: boolean; showArchiveButton: boolean; showCaptionsButton: boolean; + showChatButton: boolean; showEmojiButton: boolean; + showParticipantListButton: boolean; + showScreenShareButton: boolean; }; export type AppConfig = { @@ -52,8 +52,8 @@ export const defaultConfig: AppConfig = { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', - showParticipantList: true, - showChat: true, + showParticipantListButton: true, + showChatButton: true, showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, diff --git a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx index fbcd8ea3..3fd55651 100644 --- a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.spec.tsx @@ -14,7 +14,7 @@ const sessionContext = { } as unknown as SessionContextType; const mockConfigContext = { meetingRoomSettings: { - showChat: true, + showChatButton: true, }, } as unknown as ConfigContextType; const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; @@ -65,7 +65,7 @@ describe('ChatButton', () => { it('should not be displayed when chat is disabled', () => { mockUseConfigContext.mockReturnValue({ meetingRoomSettings: { - showChat: false, + showChatButton: false, }, } as unknown as ConfigContextType); render( {}} isOpen />); diff --git a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx index d2feeea7..1cd73b87 100644 --- a/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx +++ b/frontend/src/components/MeetingRoom/ChatButton/ChatButton.tsx @@ -31,7 +31,7 @@ const ChatButton = ({ const { meetingRoomSettings } = useConfigContext(); return ( - meetingRoomSettings.showChat && ( + meetingRoomSettings.showChatButton && ( { beforeEach(() => { mockUseConfigContext.mockReturnValue({ meetingRoomSettings: { - showParticipantList: true, + showParticipantListButton: true, }, } as unknown as ConfigContextType); }); @@ -35,10 +35,10 @@ describe('ParticipantListButton', () => { screen.getByRole('button').click(); expect(handleClick).toHaveBeenCalled(); }); - it('should not render if showParticipantList is false in config', () => { + it('should not render if showParticipantListButton is false in config', () => { mockUseConfigContext.mockReturnValue({ meetingRoomSettings: { - showParticipantList: false, + showParticipantListButton: false, }, } as unknown as ConfigContextType); render( {}} isOpen={false} participantCount={10} />); diff --git a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx index c14360a9..2707dd65 100644 --- a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx +++ b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.spec.tsx @@ -17,7 +17,7 @@ const sessionContext = { const LittleButton = () => {}} icon={} />; const mockConfigContext = { meetingRoomSettings: { - showChat: true, + showChatButton: true, }, } as unknown as ConfigContextType; const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; @@ -179,7 +179,7 @@ describe('UnreadMessagesBadge', () => { mockUseSessionContext.mockReturnValue(sessionContextWithMessages); mockUseConfigContext.mockReturnValue({ meetingRoomSettings: { - showChat: false, + showChatButton: false, }, } as unknown as ConfigContextType); diff --git a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx index 62b6a642..931c449b 100644 --- a/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx +++ b/frontend/src/components/MeetingRoom/UnreadMessagesBadge/UnreadMessagesBadge.tsx @@ -25,7 +25,7 @@ const UnreadMessagesBadge = forwardRef(function UnreadMessagesBadge( const { children, isToolbarOverflowMenuOpen, ...rest } = props; const { unreadCount } = useSessionContext(); const isInvisible = - unreadCount === 0 || isToolbarOverflowMenuOpen || !meetingRoomSettings.showChat; + unreadCount === 0 || isToolbarOverflowMenuOpen || !meetingRoomSettings.showChatButton; return ( { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', - showParticipantList: true, - showChat: true, + showParticipantListButton: true, + showChatButton: true, showScreenShareButton: true, showArchiveButton: true, showCaptionsButton: true, From 28011403de70463cfc8566987574b8e012b43a14 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:43:16 -0700 Subject: [PATCH 39/58] renaming --- config.example.json | 8 +++---- .../useConfig/useConfig.spec.tsx | 16 ++++++------- .../ConfigProvider/useConfig/useConfig.tsx | 23 ++++++++++++------- .../usePreviewPublisher.tsx | 4 ++-- .../usePublisherOptions.spec.tsx | 14 +++++------ .../usePublisherOptions.tsx | 8 +++---- .../DeviceControlButton.spec.tsx | 4 ++-- .../DeviceControlButton.tsx | 8 +++---- .../DeviceSettingsMenu.spec.tsx | 6 ++--- .../DeviceSettingsMenu/DeviceSettingsMenu.tsx | 2 +- .../CameraButton/CameraButton.spec.tsx | 2 +- .../WaitingRoom/CameraButton/CameraButton.tsx | 4 ++-- .../pages/MeetingRoom/MeetingRoom.spec.tsx | 2 +- 13 files changed, 54 insertions(+), 47 deletions(-) diff --git a/config.example.json b/config.example.json index 8b90467b..4f54f53e 100644 --- a/config.example.json +++ b/config.example.json @@ -1,9 +1,9 @@ { "videoSettings": { - "backgroundEffects": true, - "enableDisableCapableCamera": true, - "resolution": "1280x720", - "videoOnJoin": true + "allowBackgroundEffects": true, + "allowCameraControl": true, + "allowVideoOnJoin": true, + "defaultResolution": "1280x720" }, "audioSettings": { "advancedNoiseSuppression": true, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index b3d6675f..f0b853bb 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -6,10 +6,10 @@ describe('useConfig', () => { let nativeFetch: typeof global.fetch; const defaultConfig: AppConfig = { videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, + allowCameraControl: true, + defaultResolution: '1280x720', + allowVideoOnJoin: true, + allowBackgroundEffects: true, }, audioSettings: { advancedNoiseSuppression: true, @@ -59,10 +59,10 @@ describe('useConfig', () => { it('merges config.json values if loaded (mocked fetch)', async () => { const mockConfig: AppConfig = { videoSettings: { - enableDisableCapableCamera: false, - resolution: '640x480', - videoOnJoin: false, - backgroundEffects: false, + allowCameraControl: false, + defaultResolution: '640x480', + allowVideoOnJoin: false, + allowBackgroundEffects: false, }, audioSettings: { advancedNoiseSuppression: false, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 3be47b5f..795e8ada 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -2,10 +2,17 @@ import { useMemo, useEffect, useState } from 'react'; import { LayoutMode } from '../../../types/session'; export type VideoSettings = { - enableDisableCapableCamera: boolean; - resolution: '1920x1080' | '1280x960' | '1280x720' | '640x480' | '640x360' | '320x240' | '320x180'; - videoOnJoin: boolean; - backgroundEffects: boolean; + allowBackgroundEffects: boolean; + allowCameraControl: boolean; + allowVideoOnJoin: boolean; + defaultResolution: + | '1920x1080' + | '1280x960' + | '1280x720' + | '640x480' + | '640x360' + | '320x240' + | '320x180'; }; export type AudioSettings = { @@ -37,10 +44,10 @@ export type AppConfig = { export const defaultConfig: AppConfig = { videoSettings: { - enableDisableCapableCamera: true, - resolution: '1280x720', - videoOnJoin: true, - backgroundEffects: true, + allowBackgroundEffects: true, + allowCameraControl: true, + allowVideoOnJoin: true, + defaultResolution: '1280x720', }, audioSettings: { advancedNoiseSuppression: true, diff --git a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx index 9ac49c67..ed9abd11 100644 --- a/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx +++ b/frontend/src/Context/PreviewPublisherProvider/usePreviewPublisher/usePreviewPublisher.tsx @@ -242,7 +242,7 @@ const usePreviewPublisher = (): PreviewPublisherContextType => { const publisherOptions: PublisherProperties = { insertDefaultUI: false, videoFilter, - resolution: config.videoSettings.resolution, + resolution: config.videoSettings.defaultResolution, audioSource, videoSource, }; @@ -256,7 +256,7 @@ const usePreviewPublisher = (): PreviewPublisherContextType => { } }); addPublisherListeners(publisherRef.current); - }, [addPublisherListeners, config.videoSettings.resolution]); + }, [addPublisherListeners, config.videoSettings.defaultResolution]); /** * Destroys the preview publisher diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx index d2d5a383..0539b0f8 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx @@ -76,8 +76,8 @@ describe('usePublisherOptions', () => { audioOnJoin: true, }, videoSettings: { - resolution: '1280x720', - videoOnJoin: true, + defaultResolution: '1280x720', + allowVideoOnJoin: true, }, } as unknown as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); @@ -93,7 +93,7 @@ describe('usePublisherOptions', () => { const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current).toEqual({ - resolution: '1280x720', + defaultResolution: '1280x720', publishAudio: false, publishVideo: false, audioSource: undefined, @@ -136,7 +136,7 @@ describe('usePublisherOptions', () => { const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current).toEqual({ - resolution: '1280x720', + defaultResolution: '1280x720', publishAudio: true, publishVideo: true, audioSource: '68f1d1e6f11c629b1febe51a95f8f740f8ac5cd3d4c91419bd2b52bb1a9a01cd', @@ -167,7 +167,7 @@ describe('usePublisherOptions', () => { }); it('should disable video publishing', async () => { - configContext.videoSettings.videoOnJoin = false; + configContext.videoSettings.allowVideoOnJoin = false; const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current?.publishVideo).toBe(false); @@ -175,10 +175,10 @@ describe('usePublisherOptions', () => { }); it('should configure resolution from config', async () => { - configContext.videoSettings.resolution = '640x480'; + configContext.videoSettings.defaultResolution = '640x480'; const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { - expect(result.current?.resolution).toBe('640x480'); + expect(result.current?.defaultResolution).toBe('640x480'); }); }); }); diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx index b3bdeb9e..0d25bafb 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx @@ -56,8 +56,8 @@ const usePublisherOptions = (): PublisherProperties | null => { insertDefaultUI: false, name, publishAudio: config.audioSettings.audioOnJoin && publishAudio, - publishVideo: config.videoSettings.videoOnJoin && publishVideo, - resolution: config.videoSettings.resolution, + publishVideo: config.videoSettings.allowVideoOnJoin && publishVideo, + resolution: config.videoSettings.defaultResolution, audioFilter, videoFilter, videoSource, @@ -68,8 +68,8 @@ const usePublisherOptions = (): PublisherProperties | null => { setOptions(); }, [ config.audioSettings.audioOnJoin, - config.videoSettings.resolution, - config.videoSettings.videoOnJoin, + config.videoSettings.defaultResolution, + config.videoSettings.allowVideoOnJoin, user.defaultSettings, ]); diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx index 9c50c81b..7d4aae22 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx @@ -71,7 +71,7 @@ describe('DeviceControlButton', () => { enableDisableCapableMicrophone: true, }, videoSettings: { - enableDisableCapableCamera: true, + allowCameraControl: true, }, } as unknown as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); @@ -161,7 +161,7 @@ describe('DeviceControlButton', () => { }); it('is not rendered when it is configured to be disabled', async () => { - configContext.videoSettings.enableDisableCapableCamera = false; + configContext.videoSettings.allowCameraControl = false; render( ; } - if (!enableDisableCapableCamera) { + if (!allowCameraControl) { return ; } if (isVideoEnabled) { diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx index 2878b6cd..b31a01d3 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx @@ -90,10 +90,10 @@ describe('DeviceSettingsMenu Component', () => { mockedHasMediaProcessorSupport.mockReturnValue(false); configContext = { audioSettings: { - enableDisableCapableCamera: true, + allowCameraControl: true, }, videoSettings: { - backgroundEffects: true, + allowBackgroundEffects: true, }, } as unknown as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); @@ -369,7 +369,7 @@ describe('DeviceSettingsMenu Component', () => { it('does not render the dropdown separator and background effects option when the config has disabled background effects', async () => { configContext = { videoSettings: { - backgroundEffects: false, + allowBackgroundEffects: false, }, } as unknown as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx index bd2920f0..6fb92296 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx @@ -73,7 +73,7 @@ const DeviceSettingsMenu = ({ return ( <> - {hasMediaProcessorSupport() && config.videoSettings.backgroundEffects && ( + {hasMediaProcessorSupport() && config.videoSettings.allowBackgroundEffects && ( <> diff --git a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx index 588ec43b..ed6e7796 100644 --- a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx +++ b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.spec.tsx @@ -28,7 +28,7 @@ vi.mock('../../../hooks/useConfigContext', () => { return { default: () => ({ videoSettings: { - enableDisableCapableCamera: true, + allowCameraControl: true, }, }), }; diff --git a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx index fd956208..c885a735 100644 --- a/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx +++ b/frontend/src/components/WaitingRoom/CameraButton/CameraButton.tsx @@ -18,7 +18,7 @@ const CameraButton = (): ReactElement | false => { const { toggleVideo: toggleBackgroundVideoPublisher } = useBackgroundPublisherContext(); const config = useConfigContext(); const title = `Turn ${isVideoEnabled ? 'off' : 'on'} camera`; - const canEnableOrDisableCamera = config.videoSettings.enableDisableCapableCamera; + const { allowCameraControl } = config.videoSettings; const handleToggleVideo = () => { toggleVideo(); @@ -26,7 +26,7 @@ const CameraButton = (): ReactElement | false => { }; return ( - canEnableOrDisableCamera && ( + allowCameraControl && ( { return { default: () => ({ videoSettings: { - enableDisableCapableCamera: true, + allowCameraControl: true, }, audioSettings: { enableDisableCapableMicrophone: true, From f4f599928f8380b56762c46b82cf0097785cb589 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:51:43 -0700 Subject: [PATCH 40/58] fixing tests --- .../usePublisherOptions/usePublisherOptions.spec.tsx | 6 +++--- .../DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx index 0539b0f8..c8dc7133 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx @@ -93,7 +93,7 @@ describe('usePublisherOptions', () => { const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current).toEqual({ - defaultResolution: '1280x720', + resolution: '1280x720', publishAudio: false, publishVideo: false, audioSource: undefined, @@ -136,7 +136,7 @@ describe('usePublisherOptions', () => { const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current).toEqual({ - defaultResolution: '1280x720', + resolution: '1280x720', publishAudio: true, publishVideo: true, audioSource: '68f1d1e6f11c629b1febe51a95f8f740f8ac5cd3d4c91419bd2b52bb1a9a01cd', @@ -178,7 +178,7 @@ describe('usePublisherOptions', () => { configContext.videoSettings.defaultResolution = '640x480'; const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { - expect(result.current?.defaultResolution).toBe('640x480'); + expect(result.current?.resolution).toBe('640x480'); }); }); }); diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx index b31a01d3..5c7deed2 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx @@ -90,7 +90,7 @@ describe('DeviceSettingsMenu Component', () => { mockedHasMediaProcessorSupport.mockReturnValue(false); configContext = { audioSettings: { - allowCameraControl: true, + advancedNoiseSuppression: true, }, videoSettings: { allowBackgroundEffects: true, From ec9c5f008057eb3db496faa3a665bca642c9334a Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:32:59 -0700 Subject: [PATCH 41/58] fix tests. use en.json --- .../DeviceControlButton.tsx | 10 +- frontend/src/locales/en.json | 2 + yarn.lock | 198 +++++++++--------- 3 files changed, 105 insertions(+), 105 deletions(-) diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index bc7bc136..39d6c8ae 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -47,12 +47,12 @@ const DeviceControlButton = ({ let tooltipTitle: string; if (isAudio) { if (!enableDisableCapableMicrophone) { - tooltipTitle = 'Microphone control is disabled in this application'; + tooltipTitle = t('devices.audio.disabled'); } else { tooltipTitle = audioTitle; } } else if (!allowCameraControl) { - tooltipTitle = 'Camera control is disabled in this application'; + tooltipTitle = t('devices.video.disabled'); } else { tooltipTitle = videoTitle; } @@ -111,6 +111,7 @@ const DeviceControlButton = ({ )} - + Date: Thu, 18 Sep 2025 12:55:52 -0700 Subject: [PATCH 42/58] renaming --- config.example.json | 6 +++--- .../ConfigProvider/useConfig/useConfig.spec.tsx | 12 ++++++------ .../Context/ConfigProvider/useConfig/useConfig.tsx | 12 ++++++------ .../usePublisherOptions/usePublisherOptions.spec.tsx | 4 ++-- .../usePublisherOptions/usePublisherOptions.tsx | 4 ++-- .../DeviceControlButton/DeviceControlButton.spec.tsx | 4 ++-- .../DeviceControlButton/DeviceControlButton.tsx | 8 ++++---- .../DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx | 2 +- .../ReduceNoiseTestSpeakers.spec.tsx | 4 ++-- .../ReduceNoiseTestSpeakers.tsx | 2 +- .../components/WaitingRoom/MicButton/MicButton.tsx | 4 ++-- frontend/src/pages/MeetingRoom/MeetingRoom.spec.tsx | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/config.example.json b/config.example.json index 4f54f53e..d8f7ab06 100644 --- a/config.example.json +++ b/config.example.json @@ -6,9 +6,9 @@ "defaultResolution": "1280x720" }, "audioSettings": { - "advancedNoiseSuppression": true, - "audioOnJoin": true, - "enableDisableCapableMicrophone": true + "allowAdvancedNoiseSuppression": true, + "allowAudioOnJoin": true, + "allowMicrophoneControl": true }, "waitingRoomSettings": { "allowDeviceSelection": true diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index f0b853bb..0ebf0cb3 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -12,9 +12,9 @@ describe('useConfig', () => { allowBackgroundEffects: true, }, audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, + allowAdvancedNoiseSuppression: true, + allowAudioOnJoin: true, + allowMicrophoneControl: true, }, waitingRoomSettings: { allowDeviceSelection: true, @@ -65,9 +65,9 @@ describe('useConfig', () => { allowBackgroundEffects: false, }, audioSettings: { - advancedNoiseSuppression: false, - audioOnJoin: false, - enableDisableCapableMicrophone: false, + allowAdvancedNoiseSuppression: false, + allowAudioOnJoin: false, + allowMicrophoneControl: false, }, waitingRoomSettings: { allowDeviceSelection: false, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index 795e8ada..af472c4d 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -16,9 +16,9 @@ export type VideoSettings = { }; export type AudioSettings = { - advancedNoiseSuppression: boolean; - audioOnJoin: boolean; - enableDisableCapableMicrophone: boolean; + allowAdvancedNoiseSuppression: boolean; + allowAudioOnJoin: boolean; + allowMicrophoneControl: boolean; }; export type WaitingRoomSettings = { @@ -50,9 +50,9 @@ export const defaultConfig: AppConfig = { defaultResolution: '1280x720', }, audioSettings: { - advancedNoiseSuppression: true, - audioOnJoin: true, - enableDisableCapableMicrophone: true, + allowAdvancedNoiseSuppression: true, + allowAudioOnJoin: true, + allowMicrophoneControl: true, }, waitingRoomSettings: { allowDeviceSelection: true, diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx index c8dc7133..a33a5829 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.spec.tsx @@ -73,7 +73,7 @@ describe('usePublisherOptions', () => { await deviceStore.init(); configContext = { audioSettings: { - audioOnJoin: true, + allowAudioOnJoin: true, }, videoSettings: { defaultResolution: '1280x720', @@ -159,7 +159,7 @@ describe('usePublisherOptions', () => { describe('configurable features', () => { it('should disable audio publishing', async () => { - configContext.audioSettings.audioOnJoin = false; + configContext.audioSettings.allowAudioOnJoin = false; const { result } = renderHook(() => usePublisherOptions()); await waitFor(() => { expect(result.current?.publishAudio).toBe(false); diff --git a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx index 0d25bafb..4c6d8c7f 100644 --- a/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx +++ b/frontend/src/Context/PublisherProvider/usePublisherOptions/usePublisherOptions.tsx @@ -55,7 +55,7 @@ const usePublisherOptions = (): PublisherProperties | null => { initials, insertDefaultUI: false, name, - publishAudio: config.audioSettings.audioOnJoin && publishAudio, + publishAudio: config.audioSettings.allowAudioOnJoin && publishAudio, publishVideo: config.videoSettings.allowVideoOnJoin && publishVideo, resolution: config.videoSettings.defaultResolution, audioFilter, @@ -67,7 +67,7 @@ const usePublisherOptions = (): PublisherProperties | null => { setOptions(); }, [ - config.audioSettings.audioOnJoin, + config.audioSettings.allowAudioOnJoin, config.videoSettings.defaultResolution, config.videoSettings.allowVideoOnJoin, user.defaultSettings, diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx index 7d4aae22..08076461 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx @@ -68,7 +68,7 @@ describe('DeviceControlButton', () => { configContext = { audioSettings: { - enableDisableCapableMicrophone: true, + allowMicrophoneControl: true, }, videoSettings: { allowCameraControl: true, @@ -125,7 +125,7 @@ describe('DeviceControlButton', () => { }); it('renders the button as disabled with greyed out icon and correct tooltip when microphone control is disabled', async () => { - configContext.audioSettings.enableDisableCapableMicrophone = false; + configContext.audioSettings.allowMicrophoneControl = false; render( (null); const audioTitle = isAudioEnabled ? t('devices.audio.disable') : t('devices.audio.enable'); const videoTitle = isVideoEnabled ? t('devices.video.disable') : t('devices.video.enable'); - const { enableDisableCapableMicrophone } = config.audioSettings; + const { allowMicrophoneControl } = config.audioSettings; const { allowCameraControl } = config.videoSettings; - const isButtonDisabled = isAudio ? !enableDisableCapableMicrophone : !allowCameraControl; + const isButtonDisabled = isAudio ? !allowMicrophoneControl : !allowCameraControl; let tooltipTitle: string; if (isAudio) { - if (!enableDisableCapableMicrophone) { + if (!allowMicrophoneControl) { tooltipTitle = t('devices.audio.disabled'); } else { tooltipTitle = audioTitle; @@ -70,7 +70,7 @@ const DeviceControlButton = ({ const renderControlIcon = () => { if (isAudio) { - if (!enableDisableCapableMicrophone) { + if (!allowMicrophoneControl) { return ; } if (isAudioEnabled) { diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx index 5c7deed2..344df269 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx @@ -90,7 +90,7 @@ describe('DeviceSettingsMenu Component', () => { mockedHasMediaProcessorSupport.mockReturnValue(false); configContext = { audioSettings: { - advancedNoiseSuppression: true, + allowAdvancedNoiseSuppression: true, }, videoSettings: { allowBackgroundEffects: true, diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx index 3f739fb0..9d8ed9f6 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx @@ -50,7 +50,7 @@ describe('ReduceNoiseTestSpeakers', () => { } as unknown as PublisherContextType; configContext = { audioSettings: { - advancedNoiseSuppression: true, + allowAdvancedNoiseSuppression: true, }, } as unknown as ConfigContextType; mockUsePublisherContext.mockImplementation(() => publisherContext); @@ -143,7 +143,7 @@ describe('ReduceNoiseTestSpeakers', () => { it('should not render the Advanced Noise Suppression option if it is configured to be disabled', () => { configContext = { audioSettings: { - advancedNoiseSuppression: false, + allowAdvancedNoiseSuppression: false, }, } as unknown as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx index 38b86b33..4ab204bf 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx @@ -33,7 +33,7 @@ const ReduceNoiseTestSpeakers = ({ const { publisher, isPublishing } = usePublisherContext(); const config = useConfigContext(); const [isToggled, setIsToggled] = useState(false); - const isANSEnabled = config.audioSettings.advancedNoiseSuppression; + const isANSEnabled = config.audioSettings.allowAdvancedNoiseSuppression; const handleToggle = async () => { const newState = !isToggled; diff --git a/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx b/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx index 71dedd6c..9bd235d3 100644 --- a/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx +++ b/frontend/src/components/WaitingRoom/MicButton/MicButton.tsx @@ -20,10 +20,10 @@ const MicButton = (): ReactElement | false => { const title = isAudioEnabled ? t('devices.audio.microphone.state.off') : t('devices.audio.microphone.state.on'); - const canEnableOrDisableMicrophone = config.audioSettings.enableDisableCapableMicrophone; + const { allowMicrophoneControl } = config.audioSettings; return ( - canEnableOrDisableMicrophone && ( + allowMicrophoneControl && ( { allowCameraControl: true, }, audioSettings: { - enableDisableCapableMicrophone: true, + allowMicrophoneControl: true, }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', From d5e4db60495fe59bda54cb2566c1d96ce0e399f2 Mon Sep 17 00:00:00 2001 From: Christian Pettet <71297280+cpettet@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:57:20 -0700 Subject: [PATCH 43/58] fix --- .../MeetingRoom/DeviceControlButton/DeviceControlButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index ea296cc7..ac4a9f04 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -111,7 +111,6 @@ const DeviceControlButton = ({ Date: Thu, 18 Sep 2025 13:05:58 -0700 Subject: [PATCH 44/58] test fix --- .../DeviceControlButton.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index ac4a9f04..3efe8d9a 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -125,16 +125,18 @@ const DeviceControlButton = ({ )} - - {renderControlIcon()} - +
+ + {renderControlIcon()} + +
Date: Thu, 18 Sep 2025 13:35:32 -0700 Subject: [PATCH 45/58] show participant list --- config.example.json | 2 +- .../useConfig/useConfig.spec.tsx | 4 +- .../ConfigProvider/useConfig/useConfig.tsx | 4 +- .../HiddenParticipantsTile.spec.tsx | 69 +++++++++++++------ .../HiddenParticipantsTile.tsx | 13 ++-- .../ParticipantListButton.tsx | 2 +- .../ParticipantsListButton.spec.tsx | 6 +- .../VideoTileCanvas/VideoTileCanvas.tsx | 3 - .../pages/MeetingRoom/MeetingRoom.spec.tsx | 2 +- .../src/pages/MeetingRoom/MeetingRoom.tsx | 1 - 10 files changed, 67 insertions(+), 39 deletions(-) diff --git a/config.example.json b/config.example.json index d8f7ab06..8230b88a 100644 --- a/config.example.json +++ b/config.example.json @@ -19,7 +19,7 @@ "showCaptionsButton": true, "showChatButton": true, "showEmojiButton": true, - "showParticipantListButton": true, + "showParticipantList": true, "showScreenShareButton": true } } diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx index 0ebf0cb3..b8436ee7 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx @@ -21,7 +21,7 @@ describe('useConfig', () => { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', - showParticipantListButton: true, + showParticipantList: true, showChatButton: true, showScreenShareButton: true, showArchiveButton: true, @@ -74,7 +74,7 @@ describe('useConfig', () => { }, meetingRoomSettings: { defaultLayoutMode: 'grid', - showParticipantListButton: false, + showParticipantList: false, showChatButton: false, showScreenShareButton: false, showArchiveButton: false, diff --git a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx index af472c4d..7792e898 100644 --- a/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx +++ b/frontend/src/Context/ConfigProvider/useConfig/useConfig.tsx @@ -31,7 +31,7 @@ export type MeetingRoomSettings = { showCaptionsButton: boolean; showChatButton: boolean; showEmojiButton: boolean; - showParticipantListButton: boolean; + showParticipantList: boolean; showScreenShareButton: boolean; }; @@ -59,7 +59,7 @@ export const defaultConfig: AppConfig = { }, meetingRoomSettings: { defaultLayoutMode: 'active-speaker', - showParticipantListButton: true, + showParticipantList: true, showChatButton: true, showScreenShareButton: true, showArchiveButton: true, diff --git a/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.spec.tsx b/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.spec.tsx index 00fc96db..8475d1cf 100644 --- a/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.spec.tsx +++ b/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.spec.tsx @@ -1,11 +1,24 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach, Mock, afterEach } from 'vitest'; import userEvent from '@testing-library/user-event'; import { Subscriber } from '@vonage/client-sdk-video'; import { SubscriberWrapper } from '../../types/session'; import HiddenParticipantsTile from './index'; +import useConfigContext from '../../hooks/useConfigContext'; +import { ConfigContextType } from '../../Context/ConfigProvider'; + +const mockToggleParticipantList = vi.fn(); +vi.mock('../../hooks/useSessionContext', () => ({ + __esModule: true, + default: () => ({ + toggleParticipantList: mockToggleParticipantList, + }), +})); +const mockUseConfigContext = useConfigContext as Mock<[], ConfigContextType>; +vi.mock('../../hooks/useConfigContext'); describe('HiddenParticipantsTile', () => { + let configContext: ConfigContextType; const box = { height: 100, width: 100, top: 0, left: 0 }; const hiddenSubscribers = [ { @@ -28,9 +41,20 @@ describe('HiddenParticipantsTile', () => { }, ]; - it('should display two hidden participants', async () => { - const mockFn = vi.fn(); + beforeEach(() => { + configContext = { + meetingRoomSettings: { + showParticipantList: true, + }, + } as unknown as ConfigContextType; + mockUseConfigContext.mockReturnValue(configContext); + }); + afterEach(() => { + vi.resetAllMocks(); + }); + + it('should display two hidden participants', async () => { const currentHiddenSubscribers = [ ...hiddenSubscribers, { @@ -53,13 +77,7 @@ describe('HiddenParticipantsTile', () => { }, ] as SubscriberWrapper[]; - render( - - ); + render(); const button = screen.getByTestId('hidden-participants'); expect(button).toBeInTheDocument(); @@ -71,21 +89,13 @@ describe('HiddenParticipantsTile', () => { expect(screen.getByText('JD')).toBeInTheDocument(); expect(screen.getByText('JS')).toBeInTheDocument(); - expect(mockFn).toHaveBeenCalled(); + expect(mockToggleParticipantList).toHaveBeenCalled(); }); it('should display one hidden participant because the other one is empty', async () => { - const mockFn = vi.fn(); - const currentHiddenSubscribers = [...hiddenSubscribers, {}] as SubscriberWrapper[]; - render( - - ); + render(); const button = screen.getByTestId('hidden-participants'); expect(button).toBeInTheDocument(); @@ -94,6 +104,23 @@ describe('HiddenParticipantsTile', () => { expect(screen.getByText('JD')).toBeInTheDocument(); expect(screen.queryByText('JS')).not.toBeInTheDocument(); - expect(mockFn).toHaveBeenCalled(); + expect(mockToggleParticipantList).toHaveBeenCalled(); + }); + + it('does not toggle participant list when showParticipantList is disabled', async () => { + const currentHiddenSubscribers = [...hiddenSubscribers, {}] as SubscriberWrapper[]; + mockUseConfigContext.mockReturnValue({ + meetingRoomSettings: { + showParticipantList: false, + }, + } as unknown as ConfigContextType); + + render(); + + const button = screen.getByTestId('hidden-participants'); + expect(button).toBeInTheDocument(); + await userEvent.click(button); + + expect(mockToggleParticipantList).not.toHaveBeenCalled(); }); }); diff --git a/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.tsx b/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.tsx index e0239a16..f53f2476 100644 --- a/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.tsx +++ b/frontend/src/components/HiddenParticipantsTile/HiddenParticipantsTile.tsx @@ -4,11 +4,12 @@ import { Box } from 'opentok-layout-js'; import { SubscriberWrapper } from '../../types/session'; import AvatarInitials from '../AvatarInitials'; import getBoxStyle from '../../utils/helpers/getBoxStyle'; +import useSessionContext from '../../hooks/useSessionContext'; +import useConfigContext from '../../hooks/useConfigContext'; export type HiddenParticipantsTileProps = { box: Box; hiddenSubscribers: SubscriberWrapper[]; - handleClick: () => void; }; /** * HiddenParticipantsTile Component @@ -23,17 +24,21 @@ export type HiddenParticipantsTileProps = { const HiddenParticipantsTile = ({ box, hiddenSubscribers, - handleClick, }: HiddenParticipantsTileProps): ReactElement => { + const { toggleParticipantList } = useSessionContext(); + const { meetingRoomSettings } = useConfigContext(); + const { showParticipantList } = meetingRoomSettings; const { height, width } = box; const diameter = Math.min(height, width) * 0.38; return (