diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx index fcc05830..70021a37 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx @@ -1,16 +1,11 @@ import { useTranslation } from 'react-i18next' import { RiMore2Line } from '@remixicon/react' import { Button, Menu } from '@/primitives' - -import { useState } from 'react' import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems' -import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended' - -export type DialogState = 'username' | 'settings' | null export const OptionsButton = () => { const { t } = useTranslation('rooms') - const [dialogOpen, setDialogOpen] = useState(null) + return ( <> @@ -22,12 +17,8 @@ export const OptionsButton = () => { > - + - !v && setDialogOpen(null)} - /> ) } diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx index 72f5fc54..4a5546a4 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx @@ -5,20 +5,16 @@ import { } from '@remixicon/react' import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components' import { useTranslation } from 'react-i18next' -import { Dispatch, SetStateAction } from 'react' -import { DialogState } from './OptionsButton' import { Separator } from '@/primitives/Separator' import { useSidePanel } from '../../../hooks/useSidePanel' import { menuRecipe } from '@/primitives/menuRecipe.ts' +import { useSettingsDialog } from '../SettingsDialogContext' // @todo try refactoring it to use MenuList component -export const OptionsMenuItems = ({ - onOpenDialog, -}: { - onOpenDialog: Dispatch> -}) => { +export const OptionsMenuItems = () => { const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) const { toggleEffects } = useSidePanel() + const { setDialogOpen } = useSettingsDialog() return ( onOpenDialog('settings')} + onAction={() => setDialogOpen(true)} > {t('settings')} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/SettingsDialogContext.tsx b/src/frontend/src/features/rooms/livekit/components/controls/SettingsDialogContext.tsx new file mode 100644 index 00000000..ce833bfc --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/SettingsDialogContext.tsx @@ -0,0 +1,36 @@ +import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended' +import React, { createContext, useContext, useState } from 'react' + +const SettingsDialogContext = createContext< + | { + dialogOpen: boolean + setDialogOpen: React.Dispatch> + } + | undefined +>(undefined) + +export const SettingsDialogProvider: React.FC<{ + children: React.ReactNode +}> = ({ children }) => { + const [dialogOpen, setDialogOpen] = useState(false) + return ( + + {children} + !v && setDialogOpen(false)} + /> + + ) +} + +// eslint-disable-next-line react-refresh/only-export-components +export const useSettingsDialog = () => { + const context = useContext(SettingsDialogContext) + if (!context) { + throw new Error( + 'useSettingsDialog must be used within a SettingsDialogProvider' + ) + } + return context +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/TranscriptToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/TranscriptToggle.tsx index d3d1f660..24c6d520 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/TranscriptToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/TranscriptToggle.tsx @@ -4,8 +4,13 @@ import { useTranslation } from 'react-i18next' import { useSidePanel } from '../../hooks/useSidePanel' import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess' import { css } from '@/styled-system/css' +import { ToggleButtonProps } from '@/primitives/ToggleButton' -export const TranscriptToggle = () => { +export const TranscriptToggle = ({ + variant = 'primaryDark', + onPress, + ...props +}: ToggleButtonProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' }) const { isTranscriptOpen, toggleTranscript } = useSidePanel() @@ -24,11 +29,15 @@ export const TranscriptToggle = () => { > toggleTranscript()} + onPress={(e) => { + toggleTranscript() + onPress?.(e) + }} + {...props} > diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx deleted file mode 100644 index ae567613..00000000 --- a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { Track } from 'livekit-client' -import * as React from 'react' - -import { supportsScreenSharing } from '@livekit/components-core' - -import { usePersistentUserChoices } from '@livekit/components-react' - -import { StartMediaButton } from '../components/controls/StartMediaButton' -import { OptionsButton } from '../components/controls/Options/OptionsButton' -import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle' -import { ChatToggle } from '../components/controls/ChatToggle' -import { HandToggle } from '../components/controls/HandToggle' -import { SelectToggleDevice } from '../components/controls/SelectToggleDevice' -import { LeaveButton } from '../components/controls/LeaveButton' -import { ScreenShareToggle } from '../components/controls/ScreenShareToggle' -import { SupportToggle } from '../components/controls/SupportToggle' -import { TranscriptToggle } from '../components/controls/TranscriptToggle' - -import { css } from '@/styled-system/css' - -/** @public */ -export type ControlBarControls = { - microphone?: boolean - camera?: boolean - chat?: boolean - screenShare?: boolean - leave?: boolean - settings?: boolean -} - -/** @public */ -export interface ControlBarProps extends React.HTMLAttributes { - onDeviceError?: (error: { source: Track.Source; error: Error }) => void - variation?: 'minimal' | 'verbose' | 'textOnly' - controls?: ControlBarControls - /** - * If `true`, the user's device choices will be persisted. - * This will enable the user to have the same device choices when they rejoin the room. - * @defaultValue true - * @alpha - */ - saveUserChoices?: boolean -} - -/** - * The `ControlBar` prefab gives the user the basic user interface to control their - * media devices (camera, microphone and screen share), open the `Chat` and leave the room. - * - * @remarks - * This component is build with other LiveKit components like `TrackToggle`, - * `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`. - * - * @example - * ```tsx - * - * - * - * ``` - * @public - */ -export function ControlBar({ - saveUserChoices = true, - onDeviceError, -}: ControlBarProps) { - const browserSupportsScreenSharing = supportsScreenSharing() - - const { - saveAudioInputEnabled, - saveVideoInputEnabled, - saveAudioInputDeviceId, - saveVideoInputDeviceId, - } = usePersistentUserChoices({ preventSave: !saveUserChoices }) - - const microphoneOnChange = React.useCallback( - (enabled: boolean, isUserInitiated: boolean) => - isUserInitiated ? saveAudioInputEnabled(enabled) : null, - [saveAudioInputEnabled] - ) - - const cameraOnChange = React.useCallback( - (enabled: boolean, isUserInitiated: boolean) => - isUserInitiated ? saveVideoInputEnabled(enabled) : null, - [saveVideoInputEnabled] - ) - - return ( -
-
-
- - onDeviceError?.({ source: Track.Source.Microphone, error }) - } - onActiveDeviceChange={(deviceId) => - saveAudioInputDeviceId(deviceId ?? '') - } - /> - - onDeviceError?.({ source: Track.Source.Camera, error }) - } - onActiveDeviceChange={(deviceId) => - saveVideoInputDeviceId(deviceId ?? '') - } - /> - {browserSupportsScreenSharing && ( - - onDeviceError?.({ source: Track.Source.ScreenShare, error }) - } - /> - )} - - - - -
-
- - - - -
-
- ) -} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx new file mode 100644 index 00000000..c61161de --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx @@ -0,0 +1,103 @@ +import { Track } from 'livekit-client' +import * as React from 'react' +import { usePersistentUserChoices } from '@livekit/components-react' + +import { MobileControlBar } from './MobileControlBar' +import { DesktopControlBar } from './DesktopControlBar' +import { SettingsDialogProvider } from '../../components/controls/SettingsDialogContext' +import { useIsMobile } from '@/utils/useIsMobile' + +/** @public */ +export type ControlBarControls = { + microphone?: boolean + camera?: boolean + chat?: boolean + screenShare?: boolean + leave?: boolean + settings?: boolean +} + +/** @public */ +export interface ControlBarProps extends React.HTMLAttributes { + onDeviceError?: (error: { source: Track.Source; error: Error }) => void + variation?: 'minimal' | 'verbose' | 'textOnly' + controls?: ControlBarControls + /** + * If `true`, the user's device choices will be persisted. + * This will enable the user to have the same device choices when they rejoin the room. + * @defaultValue true + * @alpha + */ + saveUserChoices?: boolean +} + +/** + * The `ControlBar` prefab gives the user the basic user interface to control their + * media devices (camera, microphone and screen share), open the `Chat` and leave the room. + * + * @remarks + * This component is build with other LiveKit components like `TrackToggle`, + * `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export function ControlBar({ + saveUserChoices = true, + onDeviceError, +}: ControlBarProps) { + const { + saveAudioInputEnabled, + saveVideoInputEnabled, + saveAudioInputDeviceId, + saveVideoInputDeviceId, + } = usePersistentUserChoices({ preventSave: !saveUserChoices }) + + const microphoneOnChange = React.useCallback( + (enabled: boolean, isUserInitiated: boolean) => + isUserInitiated ? saveAudioInputEnabled(enabled) : null, + [saveAudioInputEnabled] + ) + + const cameraOnChange = React.useCallback( + (enabled: boolean, isUserInitiated: boolean) => + isUserInitiated ? saveVideoInputEnabled(enabled) : null, + [saveVideoInputEnabled] + ) + + const barProps = { + onDeviceError, + microphoneOnChange, + cameraOnChange, + saveAudioInputDeviceId, + saveVideoInputDeviceId, + } + + const isMobile = useIsMobile() + + return ( + + {isMobile ? ( + + ) : ( + + )} + + ) +} + +export interface ControlBarAuxProps { + onDeviceError: ControlBarProps['onDeviceError'] + microphoneOnChange: ( + enabled: boolean, + isUserInitiated: boolean + ) => void | null + cameraOnChange: (enabled: boolean, isUserInitiated: boolean) => void | null + saveAudioInputDeviceId: (deviceId: string) => void + saveVideoInputDeviceId: (deviceId: string) => void +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx new file mode 100644 index 00000000..bc82d28f --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx @@ -0,0 +1,106 @@ +import { supportsScreenSharing } from '@livekit/components-core' +import { ControlBarAuxProps } from './ControlBar' +import { css } from '@/styled-system/css' +import { LeaveButton } from '../../components/controls/LeaveButton' +import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice' +import { Track } from 'livekit-client' +import { HandToggle } from '../../components/controls/HandToggle' +import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle' +import { OptionsButton } from '../../components/controls/Options/OptionsButton' +import { StartMediaButton } from '../../components/controls/StartMediaButton' +import { ChatToggle } from '../../components/controls/ChatToggle' +import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle' +import { SupportToggle } from '../../components/controls/SupportToggle' +import { TranscriptToggle } from '../../components/controls/TranscriptToggle' + +export function DesktopControlBar({ + onDeviceError, + microphoneOnChange, + cameraOnChange, + saveAudioInputDeviceId, + saveVideoInputDeviceId, +}: ControlBarAuxProps) { + const browserSupportsScreenSharing = supportsScreenSharing() + return ( + <> +
+
+
+ + onDeviceError?.({ source: Track.Source.Microphone, error }) + } + onActiveDeviceChange={(deviceId) => + saveAudioInputDeviceId(deviceId ?? '') + } + /> + + onDeviceError?.({ source: Track.Source.Camera, error }) + } + onActiveDeviceChange={(deviceId) => + saveVideoInputDeviceId(deviceId ?? '') + } + /> + {browserSupportsScreenSharing && ( + + onDeviceError?.({ source: Track.Source.ScreenShare, error }) + } + /> + )} + + + + +
+
+ + + + +
+
+ + ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MobileControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MobileControlBar.tsx new file mode 100644 index 00000000..c1f38ad7 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MobileControlBar.tsx @@ -0,0 +1,182 @@ +import { supportsScreenSharing } from '@livekit/components-core' +import { useTranslation } from 'react-i18next' +import { ControlBarAuxProps } from './ControlBar' +import React from 'react' +import { css } from '@/styled-system/css' +import { LeaveButton } from '../../components/controls/LeaveButton' +import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice' +import { Track } from 'livekit-client' +import { HandToggle } from '../../components/controls/HandToggle' +import { Button } from '@/primitives/Button' +import { + RiAccountBoxLine, + RiMegaphoneLine, + RiMore2Line, + RiSettings3Line, +} from '@remixicon/react' +import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle' +import { ChatToggle } from '../../components/controls/ChatToggle' +import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle' +import { SupportToggle } from '../../components/controls/SupportToggle' +import { useSidePanel } from '../../hooks/useSidePanel' +import { LinkButton } from '@/primitives' +import { useSettingsDialog } from '../../components/controls/SettingsDialogContext' +import { ResponsiveMenu } from './ResponsiveMenu' +import { TranscriptToggle } from '../../components/controls/TranscriptToggle' + +export function MobileControlBar({ + onDeviceError, + microphoneOnChange, + cameraOnChange, + saveAudioInputDeviceId, + saveVideoInputDeviceId, +}: ControlBarAuxProps) { + const { t } = useTranslation('rooms') + const [isMenuOpened, setIsMenuOpened] = React.useState(false) + const browserSupportsScreenSharing = supportsScreenSharing() + const { toggleEffects } = useSidePanel() + const { setDialogOpen } = useSettingsDialog() + + return ( + <> +
+
+ + + onDeviceError?.({ source: Track.Source.Microphone, error }) + } + onActiveDeviceChange={(deviceId) => + saveAudioInputDeviceId(deviceId ?? '') + } + /> + + onDeviceError?.({ source: Track.Source.Camera, error }) + } + onActiveDeviceChange={(deviceId) => + saveVideoInputDeviceId(deviceId ?? '') + } + /> + + +
+
+ setIsMenuOpened(false)} + > +
+
*': { + alignSelf: 'center', + justifySelf: 'center', + }, + })} + > + {browserSupportsScreenSharing && ( + + onDeviceError?.({ source: Track.Source.ScreenShare, error }) + } + variant="primaryTextDark" + description={true} + onPress={() => setIsMenuOpened(false)} + /> + )} + setIsMenuOpened(false)} + /> + setIsMenuOpened(false)} + /> + setIsMenuOpened(false)} + /> + setIsMenuOpened(false)} + /> + + setIsMenuOpened(false)} + > + + + +
+
+
+ + ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ResponsiveMenu.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ResponsiveMenu.tsx new file mode 100644 index 00000000..5b4ae98f --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ResponsiveMenu.tsx @@ -0,0 +1,56 @@ +import { css } from '@/styled-system/css' +import { PropsWithChildren } from 'react' +import { Dialog, Modal, ModalOverlay } from 'react-aria-components' + +interface ResponsiveMenuProps extends PropsWithChildren { + isOpened: boolean + onClosed: () => void +} + +export function ResponsiveMenu({ + isOpened, + onClosed, + children, +}: ResponsiveMenuProps) { + return ( + { + if (!isOpened) { + onClosed() + } + }} + className={css({ + width: '100vw', + height: 'var(--visual-viewport-height)', + zIndex: 100, + justifyContent: 'center', + alignItems: 'flex-end', + display: 'flex', + position: 'fixed', + top: 0, + left: 0, + padding: '1.5rem 1.5rem 1rem 1.5rem', + boxSizing: 'border-box', + })} + > + + {children} + + + ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 94493cd2..9dfeb924 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -20,7 +20,7 @@ import { useCreateLayoutContext, } from '@livekit/components-react' -import { ControlBar } from './ControlBar' +import { ControlBar } from './ControlBar/ControlBar' import { styled } from '@/styled-system/jsx' import { cva } from '@/styled-system/css' import { MainNotificationToast } from '@/features/notifications/MainNotificationToast' diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 1e051cf6..1bc20436 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -29,3 +29,12 @@ body, body:has(.lk-video-conference) #crisp-chatbox > div > a { display: none !important; } + +@keyframes slide-full { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +}