Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5465f3b85 | |||
| 872ce1ecc6 | |||
| e2c3b745ca |
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1080 1080">
|
||||
<defs>
|
||||
<style>
|
||||
.st0, .st1, .st2, .st3, .st4, .st5 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: #6dd58c;
|
||||
}
|
||||
|
||||
.st6, .st1, .st2, .st7, .st8, .st9, .st10 {
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.st6, .st1, .st2, .st7, .st8, .st10 {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.st6, .st1, .st7, .st8, .st4, .st5, .st10 {
|
||||
stroke: #191c1e;
|
||||
}
|
||||
|
||||
.st11, .st7, .st12, .st9 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st11, .st3 {
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.st11, .st3, .st9 {
|
||||
stroke-width: 5px;
|
||||
}
|
||||
|
||||
.st2, .st9 {
|
||||
stroke: #898989;
|
||||
}
|
||||
|
||||
.st8 {
|
||||
fill: #ff8669;
|
||||
}
|
||||
|
||||
.st13 {
|
||||
fill: #e3e3fb;
|
||||
}
|
||||
|
||||
.st14 {
|
||||
fill: #f7f7f7;
|
||||
}
|
||||
|
||||
.st4, .st5 {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
stroke-linecap: square;
|
||||
}
|
||||
|
||||
.st15 {
|
||||
fill: #ca3632;
|
||||
}
|
||||
|
||||
.st10 {
|
||||
fill: #ffb929;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st12" x=".53" y=".53" width="1078.61" height="1080.36"/>
|
||||
<path class="st12" d="M120.5,942.36V210.83c0-39.94,32.37-72.32,72.31-72.33h767.27"/>
|
||||
<path class="st1" d="M120.5,942.36V210.83c0-39.94,32.37-72.32,72.31-72.33h767.27"/>
|
||||
<path class="st12" d="M960.08,256.93h-110c-12.94,0-23.43-10.49-23.43-23.43v-71.56c0-12.94-10.48-23.43-23.42-23.44h-266.94c-12.94,0-23.43,10.49-23.43,23.44h0v71.56c0,12.94-10.49,23.43-23.43,23.43H120.5v137.62h839.58"/>
|
||||
<path class="st8" d="M210.81,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st10" d="M274.32,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st6" d="M337.84,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st1" d="M236.18,329.44h-63.69M201.62,358.57l-29.13-29.13,29.13-29.12M274.32,329.44h63.69M308.88,358.57l29.13-29.13-29.13-29.12"/>
|
||||
<path class="st7" d="M625.31,322.08h-27.15c-4.35,0-7.88,3.53-7.88,7.88h0v17.19c0,4.35,3.53,7.88,7.88,7.88h27.15c4.35,0,7.88-3.53,7.88-7.88h0v-17.19c0-4.35-3.53-7.88-7.88-7.88h0Z"/>
|
||||
<path class="st12" d="M595.13,322.09v-12.86c-.02-9.26,7.47-16.78,16.73-16.8h.03c9.26,0,16.76,7.5,16.76,16.76v12.86"/>
|
||||
<path class="st1" d="M595.13,322.09v-12.86c-.02-9.26,7.47-16.78,16.73-16.8h.03c9.26,0,16.76,7.5,16.76,16.76v12.86"/>
|
||||
<g>
|
||||
<ellipse class="st4" cx="707.63" cy="299.5" rx="18.37" ry="18.5"/>
|
||||
<path class="st5" d="M624,299h63"/>
|
||||
<circle class="st4" cx="643.5" cy="339.5" r="18.5"/>
|
||||
<path class="st5" d="M728,339h-63"/>
|
||||
</g>
|
||||
<path class="st14" d="M192.29,138.5h767.79v118.43H120.5v-46.64c0-39.62,32.17-71.79,71.79-71.79h0Z"/>
|
||||
<path class="st0" d="M120.5,942.36V210.83c0-39.94,32.37-72.32,72.31-72.33h767.27"/>
|
||||
<path class="st2" d="M120.5,942.36V210.83c0-39.94,32.37-72.32,72.31-72.33h767.27"/>
|
||||
<path class="st2" d="M210.81,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st2" d="M274.32,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st2" d="M337.84,223.85c10.42,0,18.86-8.44,18.86-18.86s-8.44-18.86-18.86-18.86-18.86,8.44-18.86,18.86,8.44,18.86,18.86,18.86h0Z"/>
|
||||
<path class="st2" d="M236.18,329.44h-63.69M201.62,358.57l-29.13-29.13,29.13-29.12M274.32,329.44h63.69M308.88,358.57l29.13-29.13-29.13-29.12"/>
|
||||
<path class="st15" d="M382.41,636.74l39.92,39.92,13.47-13.47-188.53-188.53-13.47,13.47,11.35,11.35h-5.58c-5.26,0-9.52,4.26-9.52,9.52v133.31c0,5.26,4.26,9.52,9.52,9.52h133.31c5.26,0,9.52-4.26,9.52-9.52v-5.58h.01ZM363.37,617.69v15.1h-114.27v-114.27h15.1l99.17,99.17h0ZM439.55,633.17c0,2.02-1.19,3.6-2.78,4.33l-16.27-16.27v-75.65l-38.09,26.66v10.9l-19.04-19.04v-45.57h-45.57l-19.04-19.04h74.14c5.26,0,9.52,4.26,9.52,9.52v39.99l49.64-34.75c3.16-2.21,7.49.05,7.49,3.9v115.02h0Z"/>
|
||||
<path class="st15" d="M375.25,866.34l43.58,43.58,12.93-12.93-180.97-180.97-12.93,12.93,51.25,51.25v14.49c0,25.24,20.46,45.7,45.7,45.7,4.41,0,8.67-.62,12.7-1.79l14.17,14.17c-8.17,3.79-17.28,5.9-26.87,5.9-32.23,0-58.9-23.84-63.33-54.84h-18.43c4.21,38.13,34.49,68.4,72.62,72.62v37.06h18.28v-37.06c11.28-1.25,21.87-4.77,31.3-10.11h0ZM330.71,821.81c-11.87-1.78-21.25-11.16-23.03-23.03l23.03,23.03ZM402.21,841.85l-13.18-13.18c4.65-7.4,7.82-15.82,9.11-24.84h18.43c-1.55,14.04-6.64,27.02-14.35,38.03h0ZM375.62,815.27l-14.15-14.15c.5-2.06.76-4.21.76-6.43v-36.56c0-15.14-12.28-27.42-27.42-27.42-11.83,0-21.91,7.49-25.76,17.99l-13.68-13.68c7.94-13.52,22.63-22.59,39.44-22.59,25.24,0,45.7,20.46,45.7,45.7v36.56c0,7.4-1.76,14.39-4.88,20.58h-.01Z"/>
|
||||
<path d="M597.6,312.73c0-2.65,2.15-4.79,4.79-4.79s4.79,2.15,4.79,4.79-2.15,4.79-4.79,4.79-4.79-2.15-4.79-4.79ZM602.4,301.55c-6.18,0-11.19,5.01-11.19,11.19s5.01,11.19,11.19,11.19,11.19-5.01,11.19-11.19-5.01-11.19-11.19-11.19ZM619.98,315.93h25.57v-6.39h-25.57v6.39ZM632.76,344.69c0-2.65,2.15-4.79,4.79-4.79s4.79,2.15,4.79,4.79-2.15,4.79-4.79,4.79-4.79-2.15-4.79-4.79ZM637.56,333.51c-6.18,0-11.19,5.01-11.19,11.19s5.01,11.19,11.19,11.19,11.19-5.01,11.19-11.19-5.01-11.19-11.19-11.19ZM594.41,341.5v6.39h25.57v-6.39h-25.57Z"/>
|
||||
<line class="st2" x1="960.08" y1="394.55" x2="836.91" y2="394.55"/>
|
||||
<path class="st9" d="M527.76,394.55H120.5v-137.62h368.93c12.94,0,23.43-10.49,23.43-23.43v-71.56c0-12.95,10.49-23.44,23.43-23.44h266.94c12.94,0,23.43,10.5,23.42,23.44v71.56c0,2.09.27,4.12.79,6.05,1,3.77,2.92,7.17,5.51,9.94,4.28,4.58,10.37,7.44,17.13,7.44h110"/>
|
||||
<path class="st13" d="M645.52,274.8h314.56v101.2h-314.56c-23.81,0-43.12-22.66-43.12-50.61h0c.01-27.94,19.31-50.59,43.12-50.59h0Z"/>
|
||||
<g>
|
||||
<path class="st13" d="M851.32,326.18c0,28.02-6.82,54.45-18.9,77.71h-181.11c-43.65,0-79.03-35.38-79.03-79.03,0-21.82,8.86-41.56,23.16-55.86,14.3-14.29,34.06-23.13,55.87-23.13h179.74c12.93,23.89,20.27,51.24,20.27,80.31Z"/>
|
||||
<path d="M656.78,350.02v9.98h39.93v-9.98h-39.93ZM724.16,337.54c-9.65,0-17.47,7.82-17.47,17.47s7.82,17.46,17.47,17.46,17.47-7.82,17.47-17.46-7.82-17.47-17.47-17.47ZM724.16,362.49c-4.13,0-7.49-3.35-7.49-7.48s3.36-7.49,7.49-7.49,7.49,3.35,7.49,7.49-3.36,7.48-7.49,7.48ZM696.71,300.11v9.98h39.93v-9.98h-39.93ZM669.26,287.63c-9.65,0-17.47,7.82-17.47,17.47s7.82,17.47,17.47,17.47,17.47-7.82,17.47-17.47-7.82-17.47-17.47-17.47ZM669.26,312.59c-4.13,0-7.49-3.36-7.49-7.49s3.36-7.49,7.49-7.49,7.49,3.35,7.49,7.49-3.35,7.49-7.49,7.49Z"/>
|
||||
</g>
|
||||
<line class="st2" x1="813.4" y1="432.85" x2="551.26" y2="432.85"/>
|
||||
<path class="st3" d="M851.32,326.18c0,28.02-6.82,54.45-18.9,77.71-5.35,10.32-11.74,20.02-19.02,28.96-30.98,38.03-78.19,62.32-131.07,62.32s-100.09-24.29-131.07-62.32c-23.7-29.09-37.92-66.22-37.92-106.67,0-93.33,75.66-168.99,168.99-168.99,64.26,0,120.15,35.87,148.72,88.68,12.93,23.89,20.27,51.24,20.27,80.31h0Z"/>
|
||||
<polygon class="st11" points="705.12 406.95 681.79 637.91 746.21 597.51 802.28 751.56 852.2 733.39 796.14 579.34 871.46 568.88 705.12 406.95"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LiveKitRoom } from '@livekit/components-react'
|
||||
import {
|
||||
LiveKitRoom,
|
||||
usePersistentUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import {
|
||||
DisconnectReason,
|
||||
MediaDeviceFailure,
|
||||
@@ -31,18 +34,20 @@ import { isFireFox } from '@/utils/livekit'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
userConfig,
|
||||
initialRoomData,
|
||||
mode = 'join',
|
||||
}: {
|
||||
roomId: string
|
||||
userConfig: LocalUserChoices
|
||||
mode?: 'join' | 'create'
|
||||
initialRoomData?: ApiRoom
|
||||
}) => {
|
||||
const posthog = usePostHog()
|
||||
const { data: apiConfig } = useConfig()
|
||||
|
||||
const { userChoices: userConfig } = usePersistentUserChoices() as {
|
||||
userChoices: LocalUserChoices
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('visit-room', { slug: roomId })
|
||||
}, [roomId, posthog])
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import { usePreviewTracks } from '@livekit/components-react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { LocalVideoTrack, Track } from 'livekit-client'
|
||||
import { H } from '@/primitives/H'
|
||||
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
||||
import { Field } from '@/primitives/Field'
|
||||
import { Button, Dialog, Text, Form } from '@/primitives'
|
||||
import { HStack, VStack } from '@/styled-system/jsx'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { Heading } from 'react-aria-components'
|
||||
import { RiImageCircleAiFill } from '@remixicon/react'
|
||||
import {
|
||||
@@ -27,7 +27,6 @@ import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { ApiAccessLevel } from '../api/ApiRoom'
|
||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||
import { LocalUserChoices } from '@/stores/userChoices'
|
||||
|
||||
const onError = (e: Error) => console.error('ERROR', e)
|
||||
|
||||
@@ -72,45 +71,23 @@ const Effects = ({
|
||||
</Text>
|
||||
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
||||
</Dialog>
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: '0',
|
||||
padding: '1rem',
|
||||
zIndex: '1',
|
||||
})}
|
||||
<Button
|
||||
variant="whiteCircle"
|
||||
onPress={openDialog}
|
||||
tooltip={t('description')}
|
||||
aria-label={t('description')}
|
||||
>
|
||||
<Button
|
||||
variant="whiteCircle"
|
||||
onPress={openDialog}
|
||||
tooltip={t('description')}
|
||||
aria-label={t('description')}
|
||||
>
|
||||
<RiImageCircleAiFill size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: '20%',
|
||||
backgroundImage:
|
||||
'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(255,255,255,0) 100%)',
|
||||
borderBottomRadius: '1rem',
|
||||
})}
|
||||
/>
|
||||
<RiImageCircleAiFill size={24} />
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Join = ({
|
||||
onSubmit,
|
||||
enterRoom,
|
||||
roomId,
|
||||
}: {
|
||||
onSubmit: (choices: LocalUserChoices) => void
|
||||
enterRoom: () => void
|
||||
roomId: string
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
@@ -132,23 +109,14 @@ export const Join = ({
|
||||
saveProcessorSerialized,
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const [processor, setProcessor] = useState(
|
||||
BackgroundProcessorFactory.deserializeProcessor(processorSerialized)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
saveProcessorSerialized(processor?.serialize())
|
||||
}, [
|
||||
processor,
|
||||
saveProcessorSerialized,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify(processor?.serialize()),
|
||||
])
|
||||
|
||||
const tracks = usePreviewTracks(
|
||||
{
|
||||
audio: { deviceId: audioDeviceId },
|
||||
video: { deviceId: videoDeviceId },
|
||||
video: {
|
||||
deviceId: videoDeviceId,
|
||||
processor:
|
||||
BackgroundProcessorFactory.deserializeProcessor(processorSerialized),
|
||||
},
|
||||
},
|
||||
onError
|
||||
)
|
||||
@@ -170,12 +138,14 @@ export const Join = ({
|
||||
)
|
||||
|
||||
const videoEl = useRef(null)
|
||||
const isVideoInitiated = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoEl.current as HTMLVideoElement | null
|
||||
|
||||
const handleVideoLoaded = () => {
|
||||
if (videoElement) {
|
||||
isVideoInitiated.current = true
|
||||
videoElement.style.opacity = '1'
|
||||
}
|
||||
}
|
||||
@@ -192,25 +162,6 @@ export const Join = ({
|
||||
}
|
||||
}, [videoTrack, videoEnabled])
|
||||
|
||||
const enterRoom = useCallback(() => {
|
||||
onSubmit({
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
audioDeviceId,
|
||||
videoDeviceId,
|
||||
username,
|
||||
processorSerialized: processor?.serialize(),
|
||||
})
|
||||
}, [
|
||||
onSubmit,
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
audioDeviceId,
|
||||
videoDeviceId,
|
||||
username,
|
||||
processor,
|
||||
])
|
||||
|
||||
// Room data strategy:
|
||||
// 1. Initial fetch is performed to check access and get LiveKit configuration
|
||||
// 2. Data remains valid for 6 hours to avoid unnecessary refetches
|
||||
@@ -269,15 +220,19 @@ export const Join = ({
|
||||
enterRoom()
|
||||
}
|
||||
|
||||
// This hook is used to setup the persisted user choice processor on initialization.
|
||||
// So it's on purpose that processor is not included in the deps.
|
||||
// We just want to wait for the videoTrack to be loaded to apply the default processor.
|
||||
useEffect(() => {
|
||||
if (processor && videoTrack && !videoTrack.getProcessor()) {
|
||||
videoTrack.setProcessor(processor)
|
||||
const hintMessage = useMemo(() => {
|
||||
if (!videoEnabled) {
|
||||
return 'cameraDisabled'
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [videoTrack])
|
||||
|
||||
if (!isVideoInitiated.current) {
|
||||
return 'cameraStarting'
|
||||
}
|
||||
|
||||
if (videoTrack && videoEnabled) {
|
||||
return ''
|
||||
}
|
||||
}, [videoTrack, videoEnabled])
|
||||
|
||||
const renderWaitingState = () => {
|
||||
switch (status) {
|
||||
@@ -361,141 +316,216 @@ export const Join = ({
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
gap: { base: '1rem', sm: '2rem', lg: 0 },
|
||||
lg: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
height: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2rem',
|
||||
padding: '0 2rem',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
maxWidth: '764px',
|
||||
lg: {
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
height: '570px',
|
||||
height: '540px',
|
||||
flexGrow: 1,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
lg: {
|
||||
width: '740px',
|
||||
},
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
flexShrink: { base: 0, sm: 1 },
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
borderRadius: '1rem',
|
||||
flex: '0 1',
|
||||
minWidth: '320px',
|
||||
margin: {
|
||||
base: '0.5rem',
|
||||
sm: '1rem',
|
||||
lg: '1rem 0.5rem 1rem 1rem',
|
||||
},
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: 16 / 9,
|
||||
'--tw-shadow':
|
||||
'0 10px 15px -5px #00000010, 0 4px 5px -6px #00000010',
|
||||
'--tw-shadow-colored':
|
||||
'0 10px 15px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color)',
|
||||
boxShadow:
|
||||
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
{videoTrack && videoEnabled ? (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="1280"
|
||||
height="720"
|
||||
className={css({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transform: 'rotateY(180deg)',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
borderRadius: '1rem',
|
||||
})}
|
||||
disablePictureInPicture
|
||||
disableRemotePlayback
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
aspectRatio: '16 / 9',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
boxSizing: 'border-box',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
<div
|
||||
aria-label={t(
|
||||
`videoPreview.${videoEnabled ? 'enabled' : 'disabled'}`
|
||||
)}
|
||||
role="status"
|
||||
className={css({
|
||||
fontSize: '24px',
|
||||
fontWeight: '300',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
})}
|
||||
>
|
||||
{!videoEnabled && t('cameraDisabled')}
|
||||
{videoEnabled && !videoTrack && t('cameraStarting')}
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: '16 / 9',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: '-2px',
|
||||
left: '-2px',
|
||||
pointerEvents: 'none',
|
||||
transform: 'scale(1.02)',
|
||||
})}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="1280"
|
||||
height="720"
|
||||
style={{
|
||||
display: !videoEnabled ? 'none' : undefined,
|
||||
}}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
transform: 'rotateY(180deg)',
|
||||
opacity: 0,
|
||||
height: '100%',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
objectFit: 'cover',
|
||||
})}
|
||||
disablePictureInPicture
|
||||
disableRemotePlayback
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="alert"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '0.24rem',
|
||||
boxSizing: 'border-box',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontWeight: '400',
|
||||
fontSize: { base: '1rem', sm: '1.25rem', lg: '1.5rem' },
|
||||
textWrap: 'balance',
|
||||
color: 'white',
|
||||
})}
|
||||
>
|
||||
{hintMessage && t(hintMessage)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Effects videoTrack={videoTrack} onSubmit={setProcessor} />
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
right: '1rem',
|
||||
bottom: '1rem',
|
||||
zIndex: '1',
|
||||
})}
|
||||
>
|
||||
<Effects
|
||||
videoTrack={videoTrack}
|
||||
onSubmit={(processor) =>
|
||||
saveProcessorSerialized(processor?.serialize())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '2%',
|
||||
width: '80%',
|
||||
marginX: 'auto',
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Microphone}
|
||||
initialState={audioEnabled}
|
||||
track={audioTrack}
|
||||
initialDeviceId={audioDeviceId}
|
||||
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveAudioInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
initialState={videoEnabled}
|
||||
track={videoTrack}
|
||||
initialDeviceId={videoDeviceId}
|
||||
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveVideoInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HStack justify="center" padding={1.5}>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Microphone}
|
||||
initialState={audioEnabled}
|
||||
track={audioTrack}
|
||||
initialDeviceId={audioDeviceId}
|
||||
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveAudioInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
initialState={videoEnabled}
|
||||
track={videoTrack}
|
||||
initialDeviceId={videoDeviceId}
|
||||
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveVideoInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
flexShrink: 0,
|
||||
padding: '0',
|
||||
sm: {
|
||||
width: '448px',
|
||||
padding: '0 3rem 9rem 3rem',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{renderWaitingState()}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: '0 0 448px',
|
||||
position: 'relative',
|
||||
margin: '1rem 1rem 1rem 0.5rem',
|
||||
})}
|
||||
>
|
||||
{renderWaitingState()}
|
||||
</div>
|
||||
</div>
|
||||
</Screen>
|
||||
)
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Dialog, H } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { useModal } from '../hooks/useModal'
|
||||
|
||||
export const PermissionErrorModal = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'permissionErrorDialog' })
|
||||
const permissions = usePermissions()
|
||||
const { isOpen, close } = useModal('permissions')
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOpen() &&
|
||||
permissions.isCameraGranted &&
|
||||
permissions.isMicrophoneGranted
|
||||
) {
|
||||
close()
|
||||
}
|
||||
}, [permissions, isOpen, close])
|
||||
|
||||
// Use to split the translation in half to inject an inline icon
|
||||
const icon_placeholder = 'ICON_PLACEHOLDER'
|
||||
const [openMenuFirstPart, openMenuSecondPart] = t('body.openMenu', {
|
||||
icon_placeholder,
|
||||
}).split(icon_placeholder)
|
||||
|
||||
const permissionLabel = useMemo(() => {
|
||||
if (permissions.isMicrophoneDenied && permissions.isCameraDenied) {
|
||||
return 'cameraAndMicrophone'
|
||||
} else if (permissions.isCameraDenied) {
|
||||
return 'camera'
|
||||
} else if (permissions.isMicrophoneDenied) {
|
||||
return 'microphone'
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
}, [permissions])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen()}
|
||||
role="dialog"
|
||||
type="flex"
|
||||
title={''}
|
||||
aria-label={t(`heading.${permissionLabel}`, {
|
||||
app_title: `${import.meta.env.VITE_APP_TITLE}`,
|
||||
})}
|
||||
onClose={() => close()}
|
||||
>
|
||||
<HStack>
|
||||
<img
|
||||
src="/assets/camera_mic_permission.svg"
|
||||
alt=""
|
||||
className={css({
|
||||
minWidth: '290px',
|
||||
minHeight: '290px',
|
||||
maxWidth: '290px',
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
maxWidth: '400px',
|
||||
})}
|
||||
>
|
||||
<H lvl={2}>
|
||||
{t(`heading.${permissionLabel}`, {
|
||||
app_title: `${import.meta.env.VITE_APP_TITLE}`,
|
||||
})}
|
||||
</H>
|
||||
<ol className={css({ listStyle: 'decimal', paddingLeft: '24px' })}>
|
||||
<li>
|
||||
{openMenuFirstPart}
|
||||
<span
|
||||
style={{ display: 'inline-block', verticalAlign: 'middle' }}
|
||||
>
|
||||
<RiEqualizer2Line />
|
||||
</span>
|
||||
{openMenuSecondPart}
|
||||
</li>
|
||||
<li>{t(`body.details.${permissionLabel}`)}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</HStack>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { ModalsState, modalsStore } from '@/stores/modals'
|
||||
|
||||
interface UseModalReturn {
|
||||
isOpen: () => boolean
|
||||
open: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const useModal = (name: keyof ModalsState): UseModalReturn => {
|
||||
const modalsSnap = useSnapshot(modalsStore)
|
||||
|
||||
const isOpen = (): boolean => {
|
||||
return modalsSnap[name] as boolean
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open: () => (modalsStore[name] = true),
|
||||
close: () => (modalsStore[name] = false),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||
import { useLocation, useParams } from 'wouter'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { useUser, UserAware } from '@/features/auth'
|
||||
@@ -10,12 +9,10 @@ import {
|
||||
isRoomValid,
|
||||
normalizeRoomId,
|
||||
} from '@/features/rooms/utils/isRoomValid'
|
||||
import { LocalUserChoices } from '@/stores/userChoices'
|
||||
|
||||
export const Room = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { userChoices: existingUserChoices } = usePersistentUserChoices()
|
||||
const [userConfig, setUserConfig] = useState<LocalUserChoices | null>(null)
|
||||
const [hasSubmittedEntry, setHasSubmittedEntry] = useState(false)
|
||||
|
||||
const { roomId } = useParams()
|
||||
const [location, setLocation] = useLocation()
|
||||
@@ -48,10 +45,10 @@ export const Room = () => {
|
||||
return <ErrorScreen />
|
||||
}
|
||||
|
||||
if (!userConfig && !skipJoinScreen) {
|
||||
if (!hasSubmittedEntry && !skipJoinScreen) {
|
||||
return (
|
||||
<UserAware>
|
||||
<Join onSubmit={setUserConfig} roomId={roomId} />
|
||||
<Join enterRoom={() => setHasSubmittedEntry(true)} roomId={roomId} />
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
@@ -62,10 +59,6 @@ export const Room = () => {
|
||||
initialRoomData={initialRoomData}
|
||||
roomId={roomId}
|
||||
mode={mode}
|
||||
userConfig={{
|
||||
...existingUserChoices,
|
||||
...userConfig,
|
||||
}}
|
||||
/>
|
||||
</UserAware>
|
||||
)
|
||||
|
||||
@@ -51,22 +51,6 @@
|
||||
"body": "Niemand hat auf Ihre Anfrage reagiert"
|
||||
}
|
||||
},
|
||||
"permissionErrorDialog": {
|
||||
"heading": {
|
||||
"camera": "{{app_title}} hat keine Berechtigung, Ihre Kamera zu verwenden",
|
||||
"microphone": "{{app_title}} hat keine Berechtigung, Ihr Mikrofon zu verwenden",
|
||||
"cameraAndMicrophone": "{{app_title}} hat keine Berechtigung, Ihr Mikrofon und Ihre Kamera zu verwenden",
|
||||
"default": "{{app_title}} hat keine Berechtigung für bestimmte Zugriffe"
|
||||
},
|
||||
"body": {
|
||||
"openMenu": "Klicken Sie auf das Einstellungssymbol {{icon_placeholder}} in der Adressleiste Ihres Browsers",
|
||||
"details": {
|
||||
"camera": "Zugriff auf die Kamera erlauben",
|
||||
"microphone": "Zugriff auf das Mikrofon erlauben",
|
||||
"cameraAndMicrophone": "Zugriff auf Kamera und Mikrofon erlauben"
|
||||
}
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Dadurch verlassen Sie das Meeting.",
|
||||
"shareDialog": {
|
||||
"copy": "Meeting-Link kopieren",
|
||||
|
||||
@@ -51,22 +51,6 @@
|
||||
"body": "No one responded to your request"
|
||||
}
|
||||
},
|
||||
"permissionErrorDialog": {
|
||||
"heading": {
|
||||
"camera": "{{app_title}} is not allowed to use your camera",
|
||||
"microphone": "{{app_title}} is not allowed to use your microphone",
|
||||
"cameraAndMicrophone": "{{app_title}} is not allowed to use your microphone or camera",
|
||||
"default": "{{app_title}} is not allowed to use certain permissions"
|
||||
},
|
||||
"body": {
|
||||
"openMenu": "Click the settings icon {{icon_placeholder}} in your browser's address bar",
|
||||
"details": {
|
||||
"camera": "Allow access to the camera",
|
||||
"microphone": "Allow access to the microphone",
|
||||
"cameraAndMicrophone": "Allow access to the camera and microphone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
"shareDialog": {
|
||||
"copy": "Copy the meeting link",
|
||||
|
||||
@@ -51,22 +51,6 @@
|
||||
"body": "Personne n'a répondu à votre demande de participation à l'appel"
|
||||
}
|
||||
},
|
||||
"permissionErrorDialog": {
|
||||
"heading": {
|
||||
"camera": "{{app_title}} n'est pas autorisé à utiliser votre caméra",
|
||||
"microphone": "{{app_title}} n'est pas autorisé à utiliser votre micro",
|
||||
"cameraAndMicrophone": "{{app_title}} n'est pas autorisé à utiliser votre micro ni votre caméra",
|
||||
"default": "{{app_title}} n'est pas autorisé à utiliser certaines permissions"
|
||||
},
|
||||
"body": {
|
||||
"openMenu": "Cliquez sur l'icône des paramètres {{icon_placeholder}} dans la barre d'adresse de votre navigateur",
|
||||
"details": {
|
||||
"camera": "Autorisez l'accès à la caméra",
|
||||
"microphone": "Autorisez l'accès au microphone",
|
||||
"cameraAndMicrophone": "Autorisez l'accès à la caméra et au microphone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
"shareDialog": {
|
||||
"copy": "Copier le lien de la réunion",
|
||||
|
||||
@@ -51,22 +51,6 @@
|
||||
"body": "Niemand heeft gereageerd op uw verzoek om deel te nemen aan het gesprek"
|
||||
}
|
||||
},
|
||||
"permissionErrorDialog": {
|
||||
"heading": {
|
||||
"camera": "{{app_title}} heeft geen toestemming om je camera te gebruiken",
|
||||
"microphone": "{{app_title}} heeft geen toestemming om je microfoon te gebruiken",
|
||||
"cameraAndMicrophone": "{{app_title}} heeft geen toestemming om je microfoon en camera te gebruiken",
|
||||
"default": "{{app_title}} heeft geen toestemming voor bepaalde rechten"
|
||||
},
|
||||
"body": {
|
||||
"openMenu": "Klik op het instellingenpictogram {{icon_placeholder}} in de adresbalk van je browser",
|
||||
"details": {
|
||||
"camera": "Sta toegang tot de camera toe",
|
||||
"microphone": "Sta toegang tot de microfoon toe",
|
||||
"cameraAndMicrophone": "Sta toegang tot de camera en microfoon toe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Dat zal u de vergadering doen verlaten.",
|
||||
"shareDialog": {
|
||||
"copy": "Kopieer de vergaderlink",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { RiArrowDropDownLine } from '@remixicon/react'
|
||||
import { RemixiconComponentType, RiArrowDropDownLine } from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
ListBox,
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
import { Box } from './Box'
|
||||
import { StyledPopover } from './Popover'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
const StyledButton = styled(Button, {
|
||||
base: {
|
||||
width: 'full',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.25,
|
||||
border: '1px solid',
|
||||
@@ -53,22 +55,40 @@ const StyledSelectValue = styled(SelectValue, {
|
||||
},
|
||||
})
|
||||
|
||||
const StyledIcon = styled('div', {
|
||||
base: {
|
||||
marginRight: '0.35rem',
|
||||
flexShrink: 0,
|
||||
},
|
||||
})
|
||||
|
||||
export const Select = <T extends string | number>({
|
||||
label,
|
||||
iconComponent,
|
||||
items,
|
||||
errors,
|
||||
...props
|
||||
}: Omit<SelectProps<object>, 'items' | 'label' | 'errors'> & {
|
||||
iconComponent?: RemixiconComponentType
|
||||
label: ReactNode
|
||||
items: Array<{ value: T; label: ReactNode }>
|
||||
errors?: ReactNode
|
||||
}) => {
|
||||
const IconComponent = iconComponent
|
||||
return (
|
||||
<RACSelect {...props}>
|
||||
{label}
|
||||
<StyledButton>
|
||||
{!!IconComponent && (
|
||||
<StyledIcon>
|
||||
<IconComponent size={18} />
|
||||
</StyledIcon>
|
||||
)}
|
||||
<StyledSelectValue />
|
||||
<RiArrowDropDownLine aria-hidden="true" />
|
||||
<RiArrowDropDownLine
|
||||
aria-hidden="true"
|
||||
className={css({ flexShrink: 0 })}
|
||||
/>
|
||||
</StyledButton>
|
||||
<StyledPopover>
|
||||
<Box size="sm" type="popover" variant="control">
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
export type ModalsState = {
|
||||
permissions: boolean
|
||||
}
|
||||
|
||||
export const modalsStore = proxy<ModalsState>({
|
||||
permissions: false,
|
||||
})
|
||||
Reference in New Issue
Block a user