Compare commits

..

2 Commits

Author SHA1 Message Date
lebaudantoine 151f218e5d 📝(backend) add Firefox proxy workaround parameter to installation guide
Document LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND setting that enables
connection warmup by pre-calling WebSocket endpoint to resolve proxy
connectivity issues.
2025-08-01 16:16:35 +02:00
lebaudantoine b2767373a9 🩹(backend) allow enforcing WSS protocol to resolve browser compatibility
The LiveKit API URL is necessary to interact with the API. It uses https
protocol.

Eplicit wss protocol is necessary in Websocket constructor for some
older browsers.

This resolves critical compatibility issues with legacy browsers
(notably Firefox <124, Chrome <125, Edge <125) that lack support
for HTTPS URLs in the WebSocket() constructor. Without explicit WSS
URLs, WebSocket signaling connections may fail, crash, or be blocked
entirely in these environments.

The setting is optional and defaults to the current behavior when
not specified, ensuring zero breaking changes for existing deployments.
2025-08-01 16:16:35 +02:00
22 changed files with 273 additions and 360 deletions
+3
View File
@@ -35,6 +35,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET: secret
+3
View File
@@ -217,6 +217,9 @@ DB_NAME: meet
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
## Deployment
@@ -18,6 +18,7 @@ import {
ANIMATION_DURATION,
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata'
export const MainNotificationToast = () => {
const room = useRoomContext()
@@ -154,14 +155,17 @@ export const MainNotificationToast = () => {
useEffect(() => {
const handleNotificationReceived = (
changedAttributes: Record<string, string>,
prevMetadataStr: string | undefined,
participant: Participant
) => {
if (!participant) return
if (isMobileBrowser()) return
if (participant.isLocal) return
if (!('handRaisedAt' in changedAttributes)) return
const prevMetadata = safeParseMetadata(prevMetadataStr)
const metadata = safeParseMetadata(participant.metadata)
if (prevMetadata?.raised == metadata?.raised) return
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
@@ -169,12 +173,12 @@ export const MainNotificationToast = () => {
toast.content.type === NotificationType.HandRaised
)
if (existingToast && !changedAttributes?.handRaisedAt) {
if (existingToast && prevMetadata.raised && !metadata.raised) {
toastQueue.close(existingToast.key)
return
}
if (!existingToast && !!changedAttributes?.handRaisedAt) {
if (!existingToast && !prevMetadata.raised && metadata.raised) {
triggerNotificationSound(NotificationType.HandRaised)
toastQueue.add(
{
@@ -186,13 +190,10 @@ export const MainNotificationToast = () => {
}
}
room.on(RoomEvent.ParticipantAttributesChanged, handleNotificationReceived)
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
return () => {
room.off(
RoomEvent.ParticipantAttributesChanged,
handleNotificationReceived
)
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
}
}, [room, triggerNotificationSound])
@@ -1,17 +1,12 @@
import { useEffect, useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import {
LiveKitRoom,
usePersistentUserChoices,
} from '@livekit/components-react'
import { LiveKitRoom } from '@livekit/components-react'
import {
DisconnectReason,
MediaDeviceFailure,
Room,
RoomOptions,
supportsAdaptiveStream,
supportsDynacast,
} from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
@@ -34,20 +29,18 @@ 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])
@@ -86,13 +79,10 @@ export const Conference = ({
retry: false,
})
const isAdaptiveStreamSupported = supportsAdaptiveStream()
const isDynacastSupported = supportsDynacast()
const roomOptions = useMemo((): RoomOptions => {
return {
adaptiveStream: isAdaptiveStreamSupported,
dynacast: isDynacastSupported,
adaptiveStream: true,
dynacast: true,
publishDefaults: {
videoCodec: 'vp9',
},
@@ -104,12 +94,7 @@ export const Conference = ({
},
}
// do not rely on the userConfig object directly as its reference may change on every render
}, [
userConfig.videoDeviceId,
userConfig.audioDeviceId,
isAdaptiveStreamSupported,
isDynacastSupported,
])
}, [userConfig.videoDeviceId, userConfig.audioDeviceId])
const room = useMemo(() => new Room(roomOptions), [roomOptions])
@@ -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 { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, 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 { VStack } from '@/styled-system/jsx'
import { HStack, VStack } from '@/styled-system/jsx'
import { Heading } from 'react-aria-components'
import { RiImageCircleAiFill } from '@remixicon/react'
import {
@@ -27,6 +27,7 @@ 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)
@@ -71,23 +72,45 @@ const Effects = ({
</Text>
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
</Dialog>
<Button
variant="whiteCircle"
onPress={openDialog}
tooltip={t('description')}
aria-label={t('description')}
<div
className={css({
position: 'absolute',
right: 0,
bottom: '0',
padding: '1rem',
zIndex: '1',
})}
>
<RiImageCircleAiFill size={24} />
</Button>
<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',
})}
/>
</>
)
}
export const Join = ({
enterRoom,
onSubmit,
roomId,
}: {
enterRoom: () => void
onSubmit: (choices: LocalUserChoices) => void
roomId: string
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
@@ -109,14 +132,23 @@ 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,
processor:
BackgroundProcessorFactory.deserializeProcessor(processorSerialized),
},
video: { deviceId: videoDeviceId },
},
onError
)
@@ -138,14 +170,12 @@ 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'
}
}
@@ -162,6 +192,25 @@ 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
@@ -220,19 +269,15 @@ export const Join = ({
enterRoom()
}
const hintMessage = useMemo(() => {
if (!videoEnabled) {
return 'cameraDisabled'
// 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)
}
if (!isVideoInitiated.current) {
return 'cameraStarting'
}
if (videoTrack && videoEnabled) {
return ''
}
}, [videoTrack, videoEnabled])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoTrack])
const renderWaitingState = () => {
switch (status) {
@@ -316,215 +361,140 @@ 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: {
flexDirection: 'row',
alignItems: 'center',
},
})}
>
<div
className={css({
display: 'flex',
height: 'auto',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '2rem',
padding: '0 2rem',
flexDirection: 'column',
minWidth: 0,
maxWidth: '764px',
width: '100%',
lg: {
height: '540px',
flexGrow: 1,
flexDirection: 'row',
width: 'auto',
height: '570px',
},
})}
>
<div
className={css({
display: 'inline-flex',
flexDirection: 'column',
flexGrow: 1,
minWidth: 0,
flexShrink: { base: 0, sm: 1 },
width: '100%',
lg: {
width: '740px',
},
})}
>
<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',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
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',
})}
>
<div
className={css({
position: 'relative',
width: '100%',
height: 'fit-content',
aspectRatio: '16 / 9',
})}
>
<div
{videoTrack && videoEnabled ? (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
ref={videoEl}
width="1280"
height="720"
className={css({
backgroundColor: 'black',
position: 'absolute',
boxSizing: 'border-box',
top: 0,
display: 'block',
width: '100%',
height: '100%',
overflow: 'hidden',
objectFit: 'cover',
transform: 'rotateY(180deg)',
opacity: 0,
transition: 'opacity 0.3s ease-in-out',
borderRadius: '1rem',
})}
>
<div
aria-label={t(
`videoPreview.${videoEnabled ? 'enabled' : 'disabled'}`
)}
role="status"
className={css({
position: 'absolute',
top: 0,
width: '100%',
})}
>
<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>
disablePictureInPicture
disableRemotePlayback
/>
) : (
<div
className={css({
position: 'absolute',
right: '1rem',
bottom: '1rem',
zIndex: '1',
width: '100%',
height: '100%',
color: 'white',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
<Effects
videoTrack={videoTrack}
onSubmit={(processor) =>
saveProcessorSerialized(processor?.serialize())
}
/>
<p
className={css({
fontSize: '24px',
fontWeight: '300',
})}
>
{!videoEnabled && t('cameraDisabled')}
{videoEnabled && !videoTrack && t('cameraStarting')}
</p>
</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>
)}
<Effects videoTrack={videoTrack} onSubmit={setProcessor} />
</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>
@@ -2,6 +2,7 @@ import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata'
export const useLowerHandParticipant = () => {
const data = useRoomData()
@@ -10,12 +11,8 @@ export const useLowerHandParticipant = () => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newAttributes = {
...participant.attributes,
handRaisedAt: '',
}
const newMetadata = safeParseMetadata(participant.metadata) || {}
newMetadata.raised = !newMetadata.raised
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
@@ -27,7 +24,7 @@ export const useLowerHandParticipant = () => {
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
attributes: newAttributes,
metadata: JSON.stringify(newMetadata),
permission: participant.permissions,
}),
}
@@ -22,7 +22,7 @@ import {
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { RiHand } from '@remixicon/react'
import { useRaisedHand, useRaisedHandPosition } from '../hooks/useRaisedHand'
import { useRaisedHand } from '../hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from './MutedMicIndicator'
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
@@ -97,10 +97,6 @@ export const ParticipantTile: (
participant: trackReference.participant,
})
const { positionInQueue, firstInQueue } = useRaisedHandPosition({
participant: trackReference.participant,
})
const isScreenShare = trackReference.source != Track.Source.Camera
return (
@@ -145,32 +141,24 @@ export const ParticipantTile: (
style={{
padding: '0.1rem 0.25rem',
backgroundColor:
isHandRaised && !isScreenShare
? firstInQueue
? '#fde047'
: 'white'
: undefined,
isHandRaised && !isScreenShare ? 'white' : undefined,
color:
isHandRaised && !isScreenShare ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{isHandRaised && !isScreenShare && (
<>
<span>{positionInQueue}</span>
<RiHand
color="black"
size={16}
style={{
marginRight: '0.4rem',
marginLeft: '0.1rem',
minWidth: '16px',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
</>
<RiHand
color="black"
size={16}
style={{
marginRight: '0.4rem',
minWidth: '16px',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
)}
{isScreenShare && (
<ScreenShareIcon
@@ -11,6 +11,7 @@ import { WaitingParticipantListItem } from './WaitingParticipantListItem'
import { useWaitingParticipants } from '@/features/rooms/hooks/useWaitingParticipants'
import { Participant } from 'livekit-client'
import { WaitingParticipant } from '@/features/rooms/api/listWaitingParticipants'
import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
@@ -34,15 +35,10 @@ export const ParticipantsList = () => {
...sortedRemoteParticipants,
]
const raisedHandParticipants = participants
.filter((participant) => !!participant.attributes.handRaisedAt)
.sort((a, b) => {
const dateA = new Date(a.attributes.handRaisedAt)
const dateB = new Date(b.attributes.handRaisedAt)
const timeA = isNaN(dateA.getTime()) ? 0 : dateA.getTime()
const timeB = isNaN(dateB.getTime()) ? 0 : dateB.getTime()
return timeA - timeB
})
const raisedHandParticipants = participants.filter((participant) => {
const data = safeParseMetadata(participant.metadata)
return data.raised
})
const { waitingParticipants, handleParticipantEntry } =
useWaitingParticipants()
@@ -1,58 +1,23 @@
import { LocalParticipant, Participant } from 'livekit-client'
import {
useParticipantAttribute,
useParticipants,
} from '@livekit/components-react'
import { useParticipantInfo } from '@livekit/components-react'
import { isLocal } from '@/utils/livekit'
import { useMemo } from 'react'
type useRaisedHandProps = {
participant: Participant
}
export function useRaisedHandPosition({ participant }: useRaisedHandProps) {
const { isHandRaised } = useRaisedHand({ participant })
const participants = useParticipants()
const positionInQueue = useMemo(() => {
if (!isHandRaised) return
return (
participants
.filter((p) => !!p.attributes.handRaisedAt)
.sort((a, b) => {
const dateA = new Date(a.attributes.handRaisedAt)
const dateB = new Date(b.attributes.handRaisedAt)
return dateA.getTime() - dateB.getTime()
})
.findIndex((p) => p.identity === participant.identity) + 1
)
}, [participants, participant, isHandRaised])
return {
positionInQueue,
firstInQueue: positionInQueue == 1,
}
}
export function useRaisedHand({ participant }: useRaisedHandProps) {
const handRaisedAtAttribute = useParticipantAttribute('handRaisedAt', {
participant,
})
// fixme - refactor this part to rely on attributes
const { metadata } = useParticipantInfo({ participant })
const parsedMetadata = JSON.parse(metadata || '{}')
const isHandRaised = !!handRaisedAtAttribute
const toggleRaisedHand = async () => {
if (!isLocal(participant)) return
const localParticipant = participant as LocalParticipant
const attributes: Record<string, string> = {
handRaisedAt: !isHandRaised ? new Date().toISOString() : '',
const toggleRaisedHand = () => {
if (isLocal(participant)) {
parsedMetadata.raised = !parsedMetadata.raised
const localParticipant = participant as LocalParticipant
localParticipant.setMetadata(JSON.stringify(parsedMetadata))
}
await localParticipant.setAttributes(attributes)
}
return { isHandRaised, toggleRaisedHand }
return { isHandRaised: parsedMetadata.raised ?? false, toggleRaisedHand }
}
@@ -1,4 +1,5 @@
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'
@@ -9,10 +10,12 @@ import {
isRoomValid,
normalizeRoomId,
} from '@/features/rooms/utils/isRoomValid'
import { LocalUserChoices } from '@/stores/userChoices'
export const Room = () => {
const { isLoggedIn } = useUser()
const [hasSubmittedEntry, setHasSubmittedEntry] = useState(false)
const { userChoices: existingUserChoices } = usePersistentUserChoices()
const [userConfig, setUserConfig] = useState<LocalUserChoices | null>(null)
const { roomId } = useParams()
const [location, setLocation] = useLocation()
@@ -45,10 +48,10 @@ export const Room = () => {
return <ErrorScreen />
}
if (!hasSubmittedEntry && !skipJoinScreen) {
if (!userConfig && !skipJoinScreen) {
return (
<UserAware>
<Join enterRoom={() => setHasSubmittedEntry(true)} roomId={roomId} />
<Join onSubmit={setUserConfig} roomId={roomId} />
</UserAware>
)
}
@@ -59,6 +62,10 @@ export const Room = () => {
initialRoomData={initialRoomData}
roomId={roomId}
mode={mode}
userConfig={{
...existingUserChoices,
...userConfig,
}}
/>
</UserAware>
)
@@ -0,0 +1,22 @@
export const safeParseMetadata = (
metadataStr: string | null | undefined
): Record<string, unknown> => {
if (!metadataStr) {
return {}
}
try {
const parsed = JSON.parse(metadataStr)
// Ensure the result is an object
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
console.warn('Metadata parsed to non-object value:', parsed)
return {}
}
return parsed as Record<string, unknown>
} catch (error) {
console.error('Failed to parse metadata:', error)
return {}
}
}
+2 -22
View File
@@ -1,6 +1,6 @@
import { type ReactNode } from 'react'
import { styled } from '@/styled-system/jsx'
import { RemixiconComponentType, RiArrowDropDownLine } from '@remixicon/react'
import { RiArrowDropDownLine } from '@remixicon/react'
import {
Button,
ListBox,
@@ -12,14 +12,12 @@ 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',
@@ -55,40 +53,22 @@ 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"
className={css({ flexShrink: 0 })}
/>
<RiArrowDropDownLine aria-hidden="true" />
</StyledButton>
<StyledPopover>
<Box size="sm" type="popover" variant="control">
@@ -40,6 +40,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
@@ -40,6 +40,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
@@ -62,6 +62,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.10
version: 0.0.9
-4
View File
@@ -21,7 +21,6 @@
| `ingress.path` | Path to use for the Ingress | `/` |
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingress.tls.secretName` | Secret name for TLS config | `nil` |
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
@@ -31,7 +30,6 @@
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingressAdmin.tls.secretName` | Secret name for TLS config | `nil` |
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
@@ -165,7 +163,6 @@
| `posthog.ingress.path` | URL path prefix for the ingress routes (e.g., /) | `/` |
| `posthog.ingress.hosts` | Additional hostnames array to be included in the ingress | `[]` |
| `posthog.ingress.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
| `posthog.ingress.tls.secretName` | Secret name for TLS config | `nil` |
| `posthog.ingress.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
| `posthog.ingress.customBackends` | Custom backend service configurations for the ingress | `[]` |
| `posthog.ingress.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
@@ -175,7 +172,6 @@
| `posthog.ingressAssets.path` | URL path prefix for the ingress routes (e.g., /) | `/static` |
| `posthog.ingressAssets.hosts` | Additional hostnames array to be included in the ingress | `[]` |
| `posthog.ingressAssets.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
| `posthog.ingressAssets.tls.secretName` | Secret name for TLS config | `nil` |
| `posthog.ingressAssets.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
| `posthog.ingressAssets.customBackends` | Custom backend service configurations for the ingress | `[]` |
| `posthog.ingressAssets.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
+1 -1
View File
@@ -29,7 +29,7 @@ spec:
{{- if .Values.ingress.tls.enabled }}
tls:
{{- if .Values.ingress.host }}
- secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
- secretName: {{ $fullName }}-tls
hosts:
- {{ .Values.ingress.host | quote }}
{{- end }}
@@ -30,7 +30,6 @@ spec:
tls:
{{- if .Values.ingressAdmin.host }}
- secretName: {{ $fullName }}-tls
- secretName: {{ .Values.ingressAdmin.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
hosts:
- {{ .Values.ingressAdmin.host | quote }}
{{- end }}
+1 -1
View File
@@ -29,7 +29,7 @@ spec:
{{- if .Values.posthog.ingress.tls.enabled }}
tls:
{{- if .Values.posthog.ingress.host }}
- secretName: {{ .Values.posthog.ingress.tls.secretName | default (printf "%s-posthog-tls" $fullName) | quote }}
- secretName: {{ $fullName }}-posthog-tls
hosts:
- {{ .Values.posthog.ingress.host | quote }}
{{- end }}
@@ -29,7 +29,7 @@ spec:
{{- if .Values.posthog.ingressAssets.tls.enabled }}
tls:
{{- if .Values.posthog.ingressAssets.host }}
- secretName: {{ .Values.posthog.ingressAssets.tls.secretName | default (printf "%s-posthog-tls" $fullName) | quote }}
- secretName: {{ $fullName }}-posthog-tls
hosts:
- {{ .Values.posthog.ingressAssets.host | quote }}
{{- end }}
+1 -9
View File
@@ -38,12 +38,10 @@ ingress:
hosts: []
# - chart-example.local
## @param ingress.tls.enabled Weather to enable TLS for the Ingress
## @param ingress.tls.secretName Secret name for TLS config
## @skip ingress.tls.additional
## @extra ingress.tls.additional[].secretName Secret name for additional TLS config
## @extra ingress.tls.additional[].hosts[] Hosts for additional TLS config
tls:
secretName: null
enabled: true
additional: []
@@ -64,12 +62,10 @@ ingressAdmin:
hosts: [ ]
# - chart-example.local
## @param ingressAdmin.tls.enabled Weather to enable TLS for the Ingress
## @param ingressAdmin.tls.secretName Secret name for TLS config
## @skip ingressAdmin.tls.additional
## @extra ingressAdmin.tls.additional[].secretName Secret name for additional TLS config
## @extra ingressAdmin.tls.additional[].hosts[] Hosts for additional TLS config
tls:
secretName: null
enabled: true
additional: []
@@ -91,8 +87,8 @@ ingressMedia:
## @extra ingressMedia.tls.additional[].secretName Secret name for additional TLS config
## @extra ingressMedia.tls.additional[].hosts[] Hosts for additional TLS config
tls:
secretName: null
enabled: true
secretName: null
additional: []
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url
@@ -362,7 +358,6 @@ posthog:
## @param posthog.ingress.path URL path prefix for the ingress routes (e.g., /)
## @param posthog.ingress.hosts Additional hostnames array to be included in the ingress
## @param posthog.ingress.tls.enabled Enable or disable TLS/HTTPS for the ingress
## @param posthog.ingress.tls.secretName Secret name for TLS config
## @param posthog.ingress.tls.additional Additional TLS configurations for extra hosts/certificates
## @param posthog.ingress.customBackends Custom backend service configurations for the ingress
## @param posthog.ingress.annotations Additional Kubernetes annotations to apply to the ingress
@@ -373,7 +368,6 @@ posthog:
path: /
hosts: [ ]
tls:
secretName: null
enabled: true
additional: [ ]
@@ -386,7 +380,6 @@ posthog:
## @param posthog.ingressAssets.path URL path prefix for the ingress routes (e.g., /)
## @param posthog.ingressAssets.hosts Additional hostnames array to be included in the ingress
## @param posthog.ingressAssets.tls.enabled Enable or disable TLS/HTTPS for the ingress
## @param posthog.ingressAssets.tls.secretName Secret name for TLS config
## @param posthog.ingressAssets.tls.additional Additional TLS configurations for extra hosts/certificates
## @param posthog.ingressAssets.customBackends Custom backend service configurations for the ingress
## @param posthog.ingressAssets.annotations Additional Kubernetes annotations to apply to the ingress
@@ -397,7 +390,6 @@ posthog:
path: /static
hosts: [ ]
tls:
secretName: null
enabled: true
additional: [ ]