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
49 changed files with 351 additions and 776 deletions
-2
View File
@@ -1,2 +0,0 @@
web: bin/buildpack_start.sh
postdeploy: python manage.py migrate
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-compile script"
# Cleanup
rm -rf docker docs env.d gitlint
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-frontend script"
# Move the frontend build to the nginx root and clean up
mkdir -p build/
mv src/frontend/dist build/frontend-out
mv src/backend/* ./
mv src/nginx/* ./
echo "3.13" > .python-version
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
# Start the Django backend server
gunicorn -b :8000 meet.wsgi:application --log-file - &
# Start the Nginx server
bin/run &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
# wait for a single child to finish,
wait -n
# then kill all the other tasks
pkill -P $$
+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
+2 -5
View File
@@ -11,12 +11,11 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
from os import environ, path
from os import path
from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import sentry_sdk
from configurations import Configuration, values
from lasuite.configuration.values import SecretFileValue
@@ -87,9 +86,7 @@ class Base(Configuration):
# Database
DATABASES = {
"default": dj_database_url.config()
if environ.get("DATABASE_URL")
else {
"default": {
"ENGINE": values.Value(
"django.db.backends.postgresql_psycopg2",
environ_name="DB_ENGINE",
-1
View File
@@ -29,7 +29,6 @@ dependencies = [
"Brotli==1.1.0",
"brevo-python==1.1.2",
"celery[redis]==5.5.3",
"dj-database-url==2.3.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env python
"""Setup file for the impress module. All configuration stands in the setup.cfg file."""
# coding: utf-8
from setuptools import setup
setup()
@@ -1,187 +1,100 @@
import { useMemo, useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Bold, Button, Dialog, type DialogProps, P, Text } from '@/primitives'
import { Button, Dialog, type DialogProps, P, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
import { css } from '@/styled-system/css'
import { ApiAccessLevel, ApiRoom } from '@/features/rooms/api/ApiRoom'
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
// fixme - duplication with the InviteDialog
export const LaterMeetingDialog = ({
room,
roomId,
...dialogProps
}: { room: null | ApiRoom } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home', { keyPrefix: 'laterMeetingDialog' })
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home')
const roomUrl = getRouteUrl('room', roomId)
const roomUrl = room && getRouteUrl('room', room?.slug)
const telephony = useTelephony()
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
const isTelephonyReadyForUse = useMemo(() => {
return telephony?.enabled && room?.pin_code
}, [telephony?.enabled, room?.pin_code])
const {
isCopied,
copyRoomToClipboard,
isRoomUrlCopied,
copyRoomUrlToClipboard,
} = useCopyRoomToClipboard(room || undefined)
return (
<Dialog isOpen={!!room} {...dialogProps} title={t('heading')}>
<P>{t('description')}</P>
{!!roomUrl && (
<>
{isTelephonyReadyForUse ? (
<div
className={css({
width: '100%',
backgroundColor: 'gray.50',
borderRadius: '0.75rem',
display: 'flex',
flexDirection: 'column',
padding: '1.75rem 1.5rem',
marginTop: '0.5rem',
gap: '1rem',
overflow: 'hidden',
})}
>
<Dialog
isOpen={!!roomId}
{...dialogProps}
title={t('laterMeetingDialog.heading')}
>
<P>{t('laterMeetingDialog.description')}</P>
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('laterMeetingDialog.copy')}
style={{
justifyContent: 'start',
}}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered}
data-attr="later-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('laterMeetingDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('laterMeetingDialog.copy')
) : (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
<Text as="p" wrap="pretty">
{roomUrl?.replace(/^https?:\/\//, '')}
</Text>
{isTelephonyReadyForUse && (
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size={'sm'}
onPress={copyRoomUrlToClipboard}
aria-label={t('copyUrl')}
tooltip={t('copyUrl')}
>
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
</Button>
)}
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
})}
>
<Text as="p" wrap="pretty">
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
{telephony?.internationalPhoneNumber}
</Text>
<Text as="p" wrap="pretty">
<Bold>{t('phone.pinCode')}</Bold>{' '}
{formatPinCode(room?.pin_code)}
</Text>
</div>
<Button
variant={isCopied ? 'success' : 'tertiaryText'}
size="sm"
fullWidth
aria-label={t('copy')}
style={{
justifyContent: 'start',
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
onPress={copyRoomToClipboard}
data-attr="later-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine
style={{ marginRight: '6px', minWidth: '18px' }}
/>
{t('copy')}
</>
)}
</Button>
</div>
) : (
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('copy')}
style={{
justifyContent: 'start',
}}
onPress={copyRoomToClipboard}
onHoverChange={setIsHovered}
data-attr="later-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl?.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
)}
{room?.access_level == ApiAccessLevel.PUBLIC && (
<HStack>
<div
className={css({
backgroundColor: 'primary.200',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
})}
>
<RiSpam2Fill
size={22}
className={css({
fill: 'primary.500',
})}
/>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('permissions')}
</Text>
</HStack>
)}
</>
)}
)}
</>
)}
</Button>
<HStack>
<div
className={css({
backgroundColor: 'primary.200',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
})}
>
<RiSpam2Fill
size={22}
className={css({
fill: 'primary.500',
})}
/>
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('laterMeetingDialog.permissions')}
</Text>
</HStack>
</Dialog>
)
}
@@ -18,7 +18,6 @@ import { menuRecipe } from '@/primitives/menuRecipe.ts'
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
import { useConfig } from '@/api/useConfig'
import { LoginButton } from '@/components/LoginButton'
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
const Columns = ({ children }: { children?: ReactNode }) => {
return (
@@ -154,7 +153,7 @@ export const Home = () => {
} = usePersistentUserChoices()
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoom, setLaterRoom] = useState<null | ApiRoom>(null)
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
const { data } = useConfig()
@@ -203,7 +202,7 @@ export const Home = () => {
onAction={() => {
const slug = generateRoomId()
createRoom({ slug, username }).then((data) =>
setLaterRoom(data)
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
@@ -252,8 +251,8 @@ export const Home = () => {
</RightColumn>
</Columns>
<LaterMeetingDialog
room={laterRoom}
onOpenChange={() => setLaterRoom(null)}
roomId={laterRoomId ?? ''}
onOpenChange={() => setLaterRoomId(null)}
/>
</Screen>
</UserAware>
@@ -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])
@@ -240,6 +225,7 @@ export const Conference = ({
<InviteDialog
isOpen={showInviteDialog}
onOpenChange={setShowInviteDialog}
roomId={roomId}
onClose={() => setShowInviteDialog(false)}
/>
)}
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Div, Button, type DialogProps, P, Bold } from '@/primitives'
import { Div, Button, type DialogProps, P } from '@/primitives'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { Heading, Dialog } from 'react-aria-components'
import { Text, text } from '@/primitives/Text'
@@ -10,13 +10,8 @@ import {
RiFileCopyLine,
RiSpam2Fill,
} from '@remixicon/react'
import { useMemo } from 'react'
import { useEffect, useState } from 'react'
import { css } from '@/styled-system/css'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
// fixme - extract in a proper primitive this dialog without overlay
const StyledRACDialog = styled(Dialog, {
@@ -39,27 +34,24 @@ const StyledRACDialog = styled(Dialog, {
},
})
export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'shareDialog' })
export const InviteDialog = ({
roomId,
...dialogProps
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('rooms')
const roomUrl = getRouteUrl('room', roomId)
const roomData = useRoomData()
const roomUrl = getRouteUrl('room', roomData?.slug)
const [isCopied, setIsCopied] = useState(false)
const telephony = useTelephony()
const isTelephonyReadyForUse = useMemo(() => {
return telephony?.enabled && roomData?.pin_code
}, [telephony?.enabled, roomData?.pin_code])
const {
isCopied,
copyRoomToClipboard,
isRoomUrlCopied,
copyRoomUrlToClipboard,
} = useCopyRoomToClipboard(roomData)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
return (
<StyledRACDialog {...props}>
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VStack
alignItems="left"
@@ -68,7 +60,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
style={{ maxWidth: '100%', overflow: 'hidden' }}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
{t('heading')}
{t('shareDialog.heading')}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
@@ -76,7 +68,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
variant="tertiaryText"
size="xs"
onPress={() => {
props.onClose?.()
dialogProps.onClose?.()
close()
}}
aria-label={t('closeDialog')}
@@ -84,125 +76,49 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
<RiCloseLine />
</Button>
</Div>
<P>{t('description')}</P>
{isTelephonyReadyForUse ? (
<P>{t('shareDialog.description')}</P>
<Button
variant={isCopied ? 'success' : 'tertiary'}
fullWidth
aria-label={t('shareDialog.copy')}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
{t('shareDialog.copied')}
</>
) : (
<>
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
{t('shareDialog.copyButton')}
</>
)}
</Button>
<HStack>
<div
className={css({
width: '100%',
display: 'flex',
flexDirection: 'column',
marginTop: '0.5rem',
gap: '1rem',
overflow: 'hidden',
backgroundColor: 'primary.200',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
})}
>
<div
<RiSpam2Fill
size={22}
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fill: 'primary.500',
})}
>
<Text as="p" wrap="pretty">
{roomUrl?.replace(/^https?:\/\//, '')}
</Text>
{isTelephonyReadyForUse && roomUrl && (
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size={'sm'}
onPress={copyRoomUrlToClipboard}
aria-label={t('copyUrl')}
tooltip={t('copyUrl')}
>
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
</Button>
)}
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
})}
>
<Text as="p" wrap="pretty">
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
{telephony?.internationalPhoneNumber}
</Text>
<Text as="p" wrap="pretty">
<Bold>{t('phone.pinCode')}</Bold>{' '}
{formatPinCode(roomData?.pin_code)}
</Text>
</div>
<Button
variant={isCopied ? 'success' : 'secondaryText'}
size="sm"
fullWidth
aria-label={t('copy')}
style={{
justifyContent: 'start',
}}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine
style={{ marginRight: '6px', minWidth: '18px' }}
/>
{t('copy')}
</>
)}
</Button>
/>
</div>
) : (
<Button
variant={isCopied ? 'success' : 'tertiary'}
fullWidth
aria-label={t('copy')}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
{t('copyUrl')}
</>
)}
</Button>
)}
{roomData?.access_level === ApiAccessLevel.PUBLIC && (
<HStack>
<div
className={css({
backgroundColor: 'primary.200',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
})}
>
<RiSpam2Fill
size={22}
className={css({
fill: 'primary.500',
})}
/>
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('permissions')}
</Text>
</HStack>
)}
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('shareDialog.permissions')}
</Text>
</HStack>
</VStack>
)}
</StyledRACDialog>
@@ -2,7 +2,7 @@ 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'
@@ -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)
@@ -106,10 +107,10 @@ const Effects = ({
}
export const Join = ({
enterRoom,
onSubmit,
roomId,
}: {
enterRoom: () => void
onSubmit: (choices: LocalUserChoices) => void
roomId: string
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
@@ -131,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
)
@@ -182,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
@@ -240,6 +269,16 @@ 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)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoTrack])
const renderWaitingState = () => {
switch (status) {
case ApiLobbyStatus.TIMEOUT:
@@ -414,12 +453,7 @@ export const Join = ({
</p>
</div>
)}
<Effects
videoTrack={videoTrack}
onSubmit={(processor) =>
saveProcessorSerialized(processor?.serialize())
}
/>
<Effects videoTrack={videoTrack} onSubmit={setProcessor} />
</div>
<HStack justify="center" padding={1.5}>
<SelectToggleDevice
@@ -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,
}),
}
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'
import { useMemo } from 'react'
import { useEffect, useState } from 'react'
import { VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
@@ -8,22 +8,24 @@ import { getRouteUrl } from '@/navigation/getRouteUrl'
import { useRoomData } from '../hooks/useRoomData'
import { formatPinCode } from '../../utils/telephony'
import { useTelephony } from '../hooks/useTelephony'
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
export const Info = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const data = useRoomData()
const roomUrl = getRouteUrl('room', data?.slug)
const telephony = useTelephony()
const isTelephonyReadyForUse = useMemo(() => {
return telephony?.enabled && data?.pin_code
}, [telephony?.enabled, data?.pin_code])
const { isCopied, copyRoomToClipboard } = useCopyRoomToClipboard(data)
return (
<Div
display="flex"
@@ -51,9 +53,9 @@ export const Info = () => {
})}
>
<Text as="p" variant="xsNote" wrap="pretty">
{roomUrl.replace(/^https?:\/\//, '')}
{roomUrl}
</Text>
{isTelephonyReadyForUse && (
{telephony?.enabled && data?.pin_code && (
<>
<Text as="p" variant="xsNote" wrap="pretty">
<Bold>{t('roomInformation.phone.call')}</Bold> (
@@ -70,7 +72,10 @@ export const Info = () => {
size="sm"
variant={isCopied ? 'success' : 'tertiaryText'}
aria-label={t('roomInformation.button.ariaLabel')}
onPress={copyRoomToClipboard}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
data-attr="copy-info-sidepannel"
style={{
marginLeft: '-8px',
@@ -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,79 +0,0 @@
import { useTelephony } from './useTelephony'
import { useTranslation } from 'react-i18next'
import { useEffect, useMemo, useState } from 'react'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { getRouteUrl } from '@/navigation/getRouteUrl'
const COPY_SUCCESS_TIMEOUT = 3000
export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
const telephony = useTelephony()
const { t } = useTranslation('global', { keyPrefix: 'clipboardContent' })
const [isCopied, setIsCopied] = useState(false)
const [isRoomUrlCopied, setIsRoomUrlCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), COPY_SUCCESS_TIMEOUT)
return () => clearTimeout(timeout)
}
}, [isCopied])
useEffect(() => {
if (isRoomUrlCopied) {
const timeout = setTimeout(
() => setIsRoomUrlCopied(false),
COPY_SUCCESS_TIMEOUT
)
return () => clearTimeout(timeout)
}
}, [isRoomUrlCopied])
const roomUrl = useMemo(() => {
return room?.slug ? getRouteUrl('room', room.slug) : ''
}, [room?.slug])
const hasTelephonyInfo = useMemo(() => {
return telephony.enabled && room?.pin_code
}, [telephony.enabled, room?.pin_code])
const content = useMemo(() => {
if (!roomUrl || !room) return ''
if (!hasTelephonyInfo) return roomUrl
return [
t('url', { roomUrl }),
t('numberAndPin', {
phoneNumber: telephony?.internationalPhoneNumber,
pinCode: formatPinCode(room.pin_code),
}),
].join('\n')
}, [roomUrl, hasTelephonyInfo, telephony, room, t])
const copyRoomToClipboard = async () => {
try {
await navigator.clipboard.writeText(content)
setIsCopied(true)
} catch (error) {
console.error(error)
}
}
const copyRoomUrlToClipboard = async () => {
try {
await navigator.clipboard.writeText(roomUrl)
setIsRoomUrlCopied(true)
} catch (error) {
console.error(error)
}
}
return {
isCopied,
copyRoomToClipboard,
isRoomUrlCopied,
copyRoomUrlToClipboard,
}
}
@@ -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 {}
}
}
@@ -19,5 +19,5 @@ export const parseConfigPhoneNumber = (
}
export function formatPinCode(pinCode?: string) {
return pinCode && `${pinCode.replace(/(\d{3})(\d{3})(\d{4})/, '$1 $2 $3')}#`
return pinCode && `${pinCode.replace(/(\d{3})(\d{3})(\d{4})/, '$1 $2 $3')} #`
}
-4
View File
@@ -51,9 +51,5 @@
"ariaLabel": "Hinweis schließen",
"label": "OK"
}
},
"clipboardContent": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
}
}
+4 -9
View File
@@ -19,15 +19,10 @@
},
"laterMeetingDialog": {
"heading": "Ihre Zugangsdaten",
"description": "Teilen Sie diese Informationen mit den Gästen. Sie können dem Meeting beitreten, ohne sich anmelden zu müssen. Dieses Meeting ist dauerhaft und kann wiederverwendet werden.",
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten.",
"copy": "Informationen kopieren",
"copied": "Informationen kopiert",
"copyUrl": "Meeting-Link kopieren",
"phone": {
"call": "Rufen Sie an:",
"pinCode": "Code:"
}
"description": "Senden Sie diesen Link an die Personen, die Sie zum Meeting einladen möchten. Sie können ohne ProConnect teilnehmen.",
"copy": "Meeting-Link kopieren",
"copied": "Link in die Zwischenablage kopiert",
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten."
},
"introSlider": {
"previous": {
+7 -11
View File
@@ -53,16 +53,12 @@
},
"leaveRoomPrompt": "Dadurch verlassen Sie das Meeting.",
"shareDialog": {
"copy": "Informationen kopieren",
"copyUrl": "Link kopieren",
"copied": "Informationen kopiert",
"copy": "Meeting-Link kopieren",
"copyButton": "Link kopieren",
"copied": "Link in die Zwischenablage kopiert",
"heading": "Ihr Meeting ist bereit",
"description": "Teilen Sie diesen Link mit Personen, die Sie zum Meeting einladen möchten.",
"permissions": "Personen mit diesem Link benötigen keine Erlaubnis, um diesem Meeting beizutreten.",
"phone": {
"call": "Rufen Sie an:",
"pinCode": "Code:"
}
"permissions": "Personen mit diesem Link benötigen keine Erlaubnis, um diesem Meeting beizutreten."
},
"mediaErrorDialog": {
"DeviceInUse": {
@@ -230,9 +226,9 @@
"roomInformation": {
"title": "Verbindungsinformationen",
"button": {
"ariaLabel": "Kopieren Sie die Informationen aus Ihrer Besprechung",
"copy": "Informationen kopieren",
"copied": "Informationen kopiert"
"ariaLabel": "Kopiere deine Meeting-Adresse",
"copy": "Adresse kopieren",
"copied": "Adresse kopiert"
},
"phone": {
"call": "Rufen Sie an:",
-4
View File
@@ -51,9 +51,5 @@
"ariaLabel": "Close the suggestion",
"label": "OK"
}
},
"clipboardContent": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
}
}
+4 -9
View File
@@ -19,15 +19,10 @@
},
"laterMeetingDialog": {
"heading": "Your connection details",
"description": "Share this information with the guests. They will be able to join the meeting without needing to sign in. This meeting is permanent and can be reused.",
"permissions": "People with this link do not need your permission to join this meeting.",
"copy": "Copy information",
"copied": "Information copied to clipboard",
"copyUrl": "Copy the meeting link",
"phone": {
"call": "Call:",
"pinCode": "Code:"
}
"description": "Send this link to the people you want to invite to the meeting. They will be able to join without ProConnect.",
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
},
"introSlider": {
"previous": {
+7 -11
View File
@@ -53,16 +53,12 @@
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
"copy": "Copy information",
"copyUrl": "Copy link",
"copied": "Information copied to clipboard",
"copy": "Copy the meeting link",
"copyButton": "Copy link",
"copied": "Link copied to clipboard",
"heading": "Your meeting is ready",
"description": "Share this link with people you want to invite to the meeting.",
"permissions": "People with this link do not need your permission to join this meeting.",
"phone": {
"call": "Call:",
"pinCode": "Code:"
}
"permissions": "People with this link do not need your permission to join this meeting."
},
"mediaErrorDialog": {
"DeviceInUse": {
@@ -230,9 +226,9 @@
"roomInformation": {
"title": "Connection Information",
"button": {
"ariaLabel": "Copy the information from your meeting",
"copy": "Copy information",
"copied": "Information copied"
"ariaLabel": "Copy your meeting address",
"copy": "Copy address",
"copied": "Address copied"
},
"phone": {
"call": "Call:",
-4
View File
@@ -51,9 +51,5 @@
"ariaLabel": "Fermer la suggestion",
"label": "OK"
}
},
"clipboardContent": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
}
}
+4 -9
View File
@@ -19,15 +19,10 @@
},
"laterMeetingDialog": {
"heading": "Vos informations de connexion",
"description": "Partagez ces informations avec les invités. Ils pourront rejoindre la réunion sans avoir besoin de se connecter. Cette réunion est permanente et peut être réutilisée.",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion.",
"copy": "Copier les informations",
"copied": "Copiées dans le presse-papiers",
"copyUrl": "Copier le lien de la réunion",
"phone": {
"call": "Appelez le :",
"pinCode": "Code :"
}
"description": "Envoyez ce lien aux personnes que vous souhaitez inviter à la réunion. Ils pourront la rejoindre sans ProConnect.",
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
},
"introSlider": {
"previous": {
+8 -12
View File
@@ -53,16 +53,12 @@
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
"copy": "Copier les informations",
"copyUrl": "Copier le lien",
"copied": "Copiées dans le presse-papiers",
"copy": "Copier le lien de la réunion",
"copyButton": "Copier le lien",
"copied": "Lien copié dans le presse-papiers",
"heading": "Votre réunion est prête",
"description": "Partagez ces informations avec les personnes que vous souhaitez inviter à la réunion.",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion.",
"phone": {
"call": "Appelez le :",
"pinCode": "Code :"
}
"description": "Partagez ce lien avec les personnes que vous souhaitez inviter à la réunion.",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
},
"mediaErrorDialog": {
"DeviceInUse": {
@@ -230,9 +226,9 @@
"roomInformation": {
"title": "Informations de connexions",
"button": {
"ariaLabel": "Copier les informations de votre réunion",
"copy": "Copier les informations",
"copied": "Informations copiées"
"ariaLabel": "Copier l'adresse de votre réunion",
"copy": "Copier l'adresse",
"copied": "Adresse copiée"
},
"phone": {
"call": "Appelez le :",
-4
View File
@@ -50,9 +50,5 @@
"ariaLabel": "Sluit de suggestie",
"label": "OK"
}
},
"clipboardContent": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
}
}
+4 -9
View File
@@ -19,15 +19,10 @@
},
"laterMeetingDialog": {
"heading": "Uw verbindingsgegevens",
"description": "Deel deze informatie met de genodigden. Zij kunnen deelnemen aan de vergadering zonder zich aan te melden. Deze vergadering is permanent en kan hergebruikt worden.",
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering.",
"copy": "Informatie kopiëren",
"copied": "Informatie gekopieerd",
"copyUrl": "Kopieer de vergaderlink",
"phone": {
"call": "Bel:",
"pinCode": "Code:"
}
"description": "Stuur deze link naar de mensen die u wilt uitnodigen voor de vergadering. Zij kunnen deelnemen zonder ProConnect.",
"copy": "Kopieer de vergaderlink",
"copied": "Link gekopieerd naar klembord",
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering."
},
"introSlider": {
"previous": {
+7 -11
View File
@@ -53,16 +53,12 @@
},
"leaveRoomPrompt": "Dat zal u de vergadering doen verlaten.",
"shareDialog": {
"copy": "Informatie kopiëren",
"copyUrl": "Kopieerlink",
"copied": "Informatie gekopieerd",
"copy": "Kopieer de vergaderlink",
"copyButton": "Kopieerlink",
"copied": "Link gekopieerd naar het klembord",
"heading": "Uw vergadering is klaar",
"description": "Deel deze link met mensen die u wilt uitnodigen voor de vergadering.",
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering.",
"phone": {
"call": "Bel:",
"pinCode": "Code:"
}
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering."
},
"mediaErrorDialog": {
"DeviceInUse": {
@@ -230,9 +226,9 @@
"roomInformation": {
"title": "Verbindingsinformatie",
"button": {
"ariaLabel": "Kopieer de informatie van uw vergadering",
"copy": "Informatie kopiëren",
"copied": "Informatie gekopieerd"
"ariaLabel": "Kopieer je vergaderadres",
"copy": "Adres kopiëren",
"copied": "Adres gekopieerd"
},
"phone": {
"call": "Bel:",
+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">
@@ -68,7 +68,6 @@ export const buttonRecipe = cva({
},
secondaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
@@ -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 }}
@@ -69,8 +72,6 @@ backend:
SUMMARY_SERVICE_API_TOKEN: password
SCREEN_RECORDING_BASE_URL: https://meet.127.0.0.1.nip.io/recordings
ROOM_TELEPHONY_ENABLED: True
ROOM_TELEPHONY_DEFAULT_COUNTRY: 'FR'
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
@@ -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: [ ]
-50
View File
@@ -1,50 +0,0 @@
upstream backend_server {
server localhost:8000 fail_timeout=0;
}
server {
listen <%= ENV["PORT"] %>;
server_name _;
server_tokens off;
root /app/build/frontend-out;
# Django rest framework
location ^~ /api/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Django admin
location ^~ /admin/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
# Add no-cache headers
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
add_header Expires 0;
}
# Optionally, handle 404 errors by redirecting to index.html
error_page 404 =200 /index.html;
}