📱(front) add responsive menu on mobile
We want to have a specific responsive menu on mobile browsers. It also implied to refactor a bit the way the settings modals is opened because it could be opened from this responsive menu, so in order to achive that a specific context has been created in order to allow its opening from any sub component of the control bar.
This commit is contained in:
@@ -1,16 +1,11 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiMore2Line } from '@remixicon/react'
|
import { RiMore2Line } from '@remixicon/react'
|
||||||
import { Button, Menu } from '@/primitives'
|
import { Button, Menu } from '@/primitives'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems'
|
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 = () => {
|
export const OptionsButton = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
const [dialogOpen, setDialogOpen] = useState<DialogState>(null)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu>
|
<Menu>
|
||||||
@@ -22,12 +17,8 @@ export const OptionsButton = () => {
|
|||||||
>
|
>
|
||||||
<RiMore2Line />
|
<RiMore2Line />
|
||||||
</Button>
|
</Button>
|
||||||
<OptionsMenuItems onOpenDialog={setDialogOpen} />
|
<OptionsMenuItems />
|
||||||
</Menu>
|
</Menu>
|
||||||
<SettingsDialogExtended
|
|
||||||
isOpen={dialogOpen === 'settings'}
|
|
||||||
onOpenChange={(v) => !v && setDialogOpen(null)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,16 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dispatch, SetStateAction } from 'react'
|
|
||||||
import { DialogState } from './OptionsButton'
|
|
||||||
import { Separator } from '@/primitives/Separator'
|
import { Separator } from '@/primitives/Separator'
|
||||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||||
|
import { useSettingsDialog } from '../SettingsDialogContext'
|
||||||
|
|
||||||
// @todo try refactoring it to use MenuList component
|
// @todo try refactoring it to use MenuList component
|
||||||
export const OptionsMenuItems = ({
|
export const OptionsMenuItems = () => {
|
||||||
onOpenDialog,
|
|
||||||
}: {
|
|
||||||
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
const { toggleEffects } = useSidePanel()
|
const { toggleEffects } = useSidePanel()
|
||||||
|
const { setDialogOpen } = useSettingsDialog()
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
style={{
|
style={{
|
||||||
@@ -47,7 +43,7 @@ export const OptionsMenuItems = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuRecipe({ icon: true }).item}
|
className={menuRecipe({ icon: true }).item}
|
||||||
onAction={() => onOpenDialog('settings')}
|
onAction={() => setDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<RiSettings3Line size={20} />
|
<RiSettings3Line size={20} />
|
||||||
{t('settings')}
|
{t('settings')}
|
||||||
|
|||||||
@@ -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<React.SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
|
export const SettingsDialogProvider: React.FC<{
|
||||||
|
children: React.ReactNode
|
||||||
|
}> = ({ children }) => {
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
|
return (
|
||||||
|
<SettingsDialogContext.Provider value={{ dialogOpen, setDialogOpen }}>
|
||||||
|
{children}
|
||||||
|
<SettingsDialogExtended
|
||||||
|
isOpen={dialogOpen}
|
||||||
|
onOpenChange={(v) => !v && setDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</SettingsDialogContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -4,8 +4,13 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||||
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
|
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
|
||||||
import { css } from '@/styled-system/css'
|
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 { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
|
||||||
|
|
||||||
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
|
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
|
||||||
@@ -24,11 +29,15 @@ export const TranscriptToggle = () => {
|
|||||||
>
|
>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
variant="primaryTextDark"
|
variant={variant}
|
||||||
aria-label={t(tooltipLabel)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isTranscriptOpen}
|
isSelected={isTranscriptOpen}
|
||||||
onPress={() => toggleTranscript()}
|
onPress={(e) => {
|
||||||
|
toggleTranscript()
|
||||||
|
onPress?.(e)
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<RiBardLine />
|
<RiBardLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -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<HTMLDivElement> {
|
|
||||||
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
|
|
||||||
* <LiveKitRoom>
|
|
||||||
* <ControlBar />
|
|
||||||
* </LiveKitRoom>
|
|
||||||
* ```
|
|
||||||
* @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 (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
width: '100vw',
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
padding: '1.125rem',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
marginLeft: '0.5rem',
|
|
||||||
})}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.65rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Microphone}
|
|
||||||
onChange={microphoneOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Camera}
|
|
||||||
onChange={cameraOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Camera, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{browserSupportsScreenSharing && (
|
|
||||||
<ScreenShareToggle
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<HandToggle />
|
|
||||||
<OptionsButton />
|
|
||||||
<LeaveButton />
|
|
||||||
<StartMediaButton />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
paddingRight: '0.25rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ChatToggle />
|
|
||||||
<ParticipantsToggle />
|
|
||||||
<TranscriptToggle />
|
|
||||||
<SupportToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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
|
||||||
|
* <LiveKitRoom>
|
||||||
|
* <ControlBar />
|
||||||
|
* </LiveKitRoom>
|
||||||
|
* ```
|
||||||
|
* @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 (
|
||||||
|
<SettingsDialogProvider>
|
||||||
|
{isMobile ? (
|
||||||
|
<MobileControlBar {...barProps} />
|
||||||
|
) : (
|
||||||
|
<DesktopControlBar {...barProps} />
|
||||||
|
)}
|
||||||
|
</SettingsDialogProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '100vw',
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
padding: '1.125rem',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.65rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Microphone}
|
||||||
|
onChange={microphoneOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Camera}
|
||||||
|
onChange={cameraOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{browserSupportsScreenSharing && (
|
||||||
|
<ScreenShareToggle
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HandToggle />
|
||||||
|
<OptionsButton />
|
||||||
|
<LeaveButton />
|
||||||
|
<StartMediaButton />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
paddingRight: '0.25rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ChatToggle />
|
||||||
|
<ParticipantsToggle />
|
||||||
|
<TranscriptToggle />
|
||||||
|
<SupportToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '100vw',
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
padding: '1.125rem',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '422px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<LeaveButton />
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Microphone}
|
||||||
|
onChange={microphoneOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Camera}
|
||||||
|
onChange={cameraOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<HandToggle />
|
||||||
|
<Button
|
||||||
|
square
|
||||||
|
variant="primaryDark"
|
||||||
|
aria-label={t('options.buttonLabel')}
|
||||||
|
tooltip={t('options.buttonLabel')}
|
||||||
|
onPress={() => setIsMenuOpened(true)}
|
||||||
|
>
|
||||||
|
<RiMore2Line />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ResponsiveMenu
|
||||||
|
isOpened={isMenuOpened}
|
||||||
|
onClosed={() => setIsMenuOpened(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
|
||||||
|
gridGap: '1rem',
|
||||||
|
'& > *': {
|
||||||
|
alignSelf: 'center',
|
||||||
|
justifySelf: 'center',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{browserSupportsScreenSharing && (
|
||||||
|
<ScreenShareToggle
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
||||||
|
}
|
||||||
|
variant="primaryTextDark"
|
||||||
|
description={true}
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ChatToggle
|
||||||
|
description={true}
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
/>
|
||||||
|
<ParticipantsToggle
|
||||||
|
description={true}
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
/>
|
||||||
|
<TranscriptToggle
|
||||||
|
description={true}
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
/>
|
||||||
|
<SupportToggle
|
||||||
|
description={true}
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
toggleEffects()
|
||||||
|
setIsMenuOpened(false)
|
||||||
|
}}
|
||||||
|
variant="primaryTextDark"
|
||||||
|
aria-label={t('options.items.effects')}
|
||||||
|
tooltip={t('options.items.effects')}
|
||||||
|
description={true}
|
||||||
|
>
|
||||||
|
<RiAccountBoxLine size={20} />
|
||||||
|
</Button>
|
||||||
|
<LinkButton
|
||||||
|
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||||
|
variant="primaryTextDark"
|
||||||
|
tooltip={t('options.items.feedbacks')}
|
||||||
|
aria-label={t('options.items.feedbacks')}
|
||||||
|
description={true}
|
||||||
|
target="_blank"
|
||||||
|
onPress={() => setIsMenuOpened(false)}
|
||||||
|
>
|
||||||
|
<RiMegaphoneLine size={20} />
|
||||||
|
</LinkButton>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
setDialogOpen(true)
|
||||||
|
setIsMenuOpened(false)
|
||||||
|
}}
|
||||||
|
variant="primaryTextDark"
|
||||||
|
aria-label={t('options.items.settings')}
|
||||||
|
tooltip={t('options.items.settings')}
|
||||||
|
description={true}
|
||||||
|
>
|
||||||
|
<RiSettings3Line size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResponsiveMenu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<ModalOverlay
|
||||||
|
isDismissable
|
||||||
|
isOpen={isOpened}
|
||||||
|
onOpenChange={(isOpened) => {
|
||||||
|
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',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
className={css({
|
||||||
|
backgroundColor: 'primaryDark.200',
|
||||||
|
borderRadius: '20px',
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: '1.5rem',
|
||||||
|
'&[data-entering]': {
|
||||||
|
animation: 'slide-full 200ms',
|
||||||
|
},
|
||||||
|
'&[data-exiting]': {
|
||||||
|
animation: 'slide-full 200ms reverse',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Dialog>{children}</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
useCreateLayoutContext,
|
useCreateLayoutContext,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
|
|
||||||
import { ControlBar } from './ControlBar'
|
import { ControlBar } from './ControlBar/ControlBar'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { cva } from '@/styled-system/css'
|
import { cva } from '@/styled-system/css'
|
||||||
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
||||||
|
|||||||
@@ -29,3 +29,12 @@ body,
|
|||||||
body:has(.lk-video-conference) #crisp-chatbox > div > a {
|
body:has(.lk-video-conference) #crisp-chatbox > div > a {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slide-full {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user