Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19df135c0e | |||
| 7ccd3c146d | |||
| d2a0ddfc7b | |||
| e391d434eb |
@@ -5,8 +5,12 @@ import { css } from '@/styled-system/css'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { chatStore } from '@/stores/chat'
|
||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const ChatToggle = () => {
|
||||
export const ChatToggle = ({
|
||||
onPress,
|
||||
...props
|
||||
}: Partial<ToggleButtonProps>) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
|
||||
|
||||
const chatSnap = useSnapshot(chatStore)
|
||||
@@ -27,8 +31,12 @@ export const ChatToggle = () => {
|
||||
aria-label={t(tooltipLabel)}
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isChatOpen}
|
||||
onPress={() => toggleChat()}
|
||||
onPress={(e) => {
|
||||
toggleChat()
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr={`controls-chat-${tooltipLabel}`}
|
||||
{...props}
|
||||
>
|
||||
<RiChat1Line />
|
||||
</ToggleButton>
|
||||
|
||||
+2
-11
@@ -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<DialogState>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu>
|
||||
@@ -22,12 +17,8 @@ export const OptionsButton = () => {
|
||||
>
|
||||
<RiMore2Line />
|
||||
</Button>
|
||||
<OptionsMenuItems onOpenDialog={setDialogOpen} />
|
||||
<OptionsMenuItems />
|
||||
</Menu>
|
||||
<SettingsDialogExtended
|
||||
isOpen={dialogOpen === 'settings'}
|
||||
onOpenChange={(v) => !v && setDialogOpen(null)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-8
@@ -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<SetStateAction<DialogState>>
|
||||
}) => {
|
||||
export const OptionsMenuItems = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { toggleEffects } = useSidePanel()
|
||||
const { setDialogOpen } = useSettingsDialog()
|
||||
return (
|
||||
<RACMenu
|
||||
style={{
|
||||
@@ -47,7 +43,7 @@ export const OptionsMenuItems = ({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={() => onOpenDialog('settings')}
|
||||
onAction={() => setDialogOpen(true)}
|
||||
>
|
||||
<RiSettings3Line size={20} />
|
||||
{t('settings')}
|
||||
|
||||
+10
-2
@@ -4,8 +4,12 @@ import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useParticipants } from '@livekit/components-react'
|
||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const ParticipantsToggle = () => {
|
||||
export const ParticipantsToggle = ({
|
||||
onPress,
|
||||
...props
|
||||
}: ToggleButtonProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
|
||||
|
||||
/**
|
||||
@@ -33,8 +37,12 @@ export const ParticipantsToggle = () => {
|
||||
aria-label={t(tooltipLabel)}
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isParticipantsOpen}
|
||||
onPress={() => toggleParticipants()}
|
||||
onPress={(e) => {
|
||||
toggleParticipants()
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr={`controls-participants-${tooltipLabel}`}
|
||||
{...props}
|
||||
>
|
||||
<RiGroupLine />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -4,13 +4,21 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||
import { Track } from 'livekit-client'
|
||||
import React from 'react'
|
||||
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const ScreenShareToggle = (
|
||||
props: Omit<
|
||||
UseTrackToggleProps<Track.Source.ScreenShare>,
|
||||
'source' | 'captureOptions'
|
||||
>
|
||||
) => {
|
||||
type Props = Omit<
|
||||
UseTrackToggleProps<Track.Source.ScreenShare>,
|
||||
'source' | 'captureOptions'
|
||||
> &
|
||||
Pick<NonNullable<ButtonRecipeProps>, 'variant'> &
|
||||
ToggleButtonProps
|
||||
|
||||
export const ScreenShareToggle = ({
|
||||
variant = 'primaryDark',
|
||||
onPress,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
|
||||
const { buttonProps, enabled } = useTrackToggle({
|
||||
...props,
|
||||
@@ -26,18 +34,16 @@ export const ScreenShareToggle = (
|
||||
<ToggleButton
|
||||
isSelected={enabled}
|
||||
square
|
||||
variant="primaryDark"
|
||||
variant={variant}
|
||||
tooltip={t(tooltipLabel)}
|
||||
onPress={(e) =>
|
||||
onPress={(e) => {
|
||||
buttonProps.onClick?.(
|
||||
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
)
|
||||
}
|
||||
style={{
|
||||
maxWidth: '46px',
|
||||
maxHeight: '46px',
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr={`controls-screenshare-${tooltipLabel}`}
|
||||
{...props}
|
||||
>
|
||||
<Div position="relative">
|
||||
<RiRectangleLine size={28} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import { RiQuestionLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Crisp } from 'crisp-sdk-web'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const SupportToggle = () => {
|
||||
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
||||
const [isOpened, setIsOpened] = useState($crisp.is('chat:opened'))
|
||||
|
||||
@@ -32,8 +33,16 @@ export const SupportToggle = () => {
|
||||
tooltip={t('support')}
|
||||
aria-label={t('support')}
|
||||
isSelected={isOpened}
|
||||
onPress={() => (isOpened ? Crisp.chat.close() : Crisp.chat.open())}
|
||||
onPress={(e) => {
|
||||
if (isOpened) {
|
||||
Crisp.chat.close()
|
||||
} else {
|
||||
Crisp.chat.open()
|
||||
}
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr="controls-support"
|
||||
{...props}
|
||||
>
|
||||
<RiQuestionLine />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -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 = () => {
|
||||
>
|
||||
<ToggleButton
|
||||
square
|
||||
variant="primaryTextDark"
|
||||
variant={variant}
|
||||
aria-label={t(tooltipLabel)}
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isTranscriptOpen}
|
||||
onPress={() => toggleTranscript()}
|
||||
onPress={(e) => {
|
||||
toggleTranscript()
|
||||
onPress?.(e)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<RiBardLine />
|
||||
</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,
|
||||
} 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'
|
||||
|
||||
@@ -5,10 +5,14 @@ import {
|
||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
RACButtonsProps &
|
||||
TooltipWrapperProps
|
||||
TooltipWrapperProps & {
|
||||
// Use tooltip as description below the button.
|
||||
description?: boolean
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
tooltip,
|
||||
@@ -22,7 +26,10 @@ export const Button = ({
|
||||
<RACButton
|
||||
className={buttonRecipe(variantProps)}
|
||||
{...(componentProps as RACButtonsProps)}
|
||||
/>
|
||||
>
|
||||
{componentProps.children as ReactNode}
|
||||
{props.description && <span>{tooltip}</span>}
|
||||
</RACButton>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ import { Link, LinkProps } from 'react-aria-components'
|
||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
LinkProps &
|
||||
TooltipWrapperProps
|
||||
TooltipWrapperProps & {
|
||||
// Use tooltip as description below the button.
|
||||
description?: boolean
|
||||
}
|
||||
|
||||
export const LinkButton = ({
|
||||
tooltip,
|
||||
@@ -16,7 +20,12 @@ export const LinkButton = ({
|
||||
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||
<Link className={buttonRecipe(variantProps)} {...componentProps}>
|
||||
<>
|
||||
{componentProps.children as ReactNode}
|
||||
{props.description && <span>{tooltip}</span>}
|
||||
</>
|
||||
</Link>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import {
|
||||
ToggleButton as RACToggleButton,
|
||||
ToggleButtonProps,
|
||||
ToggleButtonProps as RACToggleButtonProps,
|
||||
} from 'react-aria-components'
|
||||
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export type ToggleButtonProps = RACToggleButtonProps &
|
||||
ButtonRecipeProps &
|
||||
TooltipWrapperProps & {
|
||||
// Use tooltip as description below the button.
|
||||
description?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* React aria ToggleButton with our button styles, that can take a tooltip if needed
|
||||
@@ -12,14 +20,20 @@ export const ToggleButton = ({
|
||||
tooltip,
|
||||
tooltipType,
|
||||
...props
|
||||
}: ToggleButtonProps & ButtonRecipeProps & TooltipWrapperProps) => {
|
||||
}: ToggleButtonProps) => {
|
||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACToggleButton
|
||||
{...componentProps}
|
||||
className={buttonRecipe(variantProps)}
|
||||
/>
|
||||
>
|
||||
<>
|
||||
{componentProps.children as ReactNode}
|
||||
{props.description && <span>{tooltip}</span>}
|
||||
</>
|
||||
</RACToggleButton>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { type RecipeVariantProps, cva } from '@/styled-system/css'
|
||||
|
||||
export type ButtonRecipe = typeof buttonRecipe
|
||||
|
||||
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
||||
|
||||
export const buttonRecipe = cva({
|
||||
base: {
|
||||
display: 'flex',
|
||||
@@ -222,6 +218,16 @@ export const buttonRecipe = cva({
|
||||
shySelected: {
|
||||
true: {},
|
||||
},
|
||||
description: {
|
||||
true: {
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
'& span': {
|
||||
fontSize: '13px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
// if the button is next to other ones to make a "button group", tell where the button is to handle radius
|
||||
groupPosition: {
|
||||
left: {
|
||||
@@ -255,3 +261,7 @@ export const buttonRecipe = cva({
|
||||
variant: 'primary',
|
||||
},
|
||||
})
|
||||
|
||||
export type ButtonRecipe = typeof buttonRecipe
|
||||
|
||||
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { isMobileBrowser } from '@livekit/components-core'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useIsMobile = () => {
|
||||
const [isMobile, setIsMobile] = useState(isMobileBrowser())
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(isMobileBrowser())
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isMobile
|
||||
}
|
||||
Reference in New Issue
Block a user