Compare commits

..

2 Commits

Author SHA1 Message Date
lebaudantoine 309333d1ba wip 2025-08-01 17:48:40 +02:00
lebaudantoine 9384b31e20 wip add store and hook to gloablly manage model openness when necessary 2025-08-01 17:47:42 +02:00
36 changed files with 558 additions and 650 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 $$
+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()
@@ -0,0 +1,109 @@
<?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>

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -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>
@@ -1,10 +1,7 @@
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,
@@ -34,20 +31,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])
@@ -240,6 +235,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
@@ -0,0 +1,90 @@
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>
)
}
@@ -0,0 +1,22 @@
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,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',
@@ -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,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>
)
@@ -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": {
+23 -11
View File
@@ -51,18 +51,30 @@
"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": "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 +242,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": {
+23 -11
View File
@@ -51,18 +51,30 @@
"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 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 +242,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": {
+24 -12
View File
@@ -51,18 +51,30 @@
"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 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 +242,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": {
+23 -11
View File
@@ -51,18 +51,30 @@
"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": "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 +242,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',
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
export type ModalsState = {
permissions: boolean
}
export const modalsStore = proxy<ModalsState>({
permissions: false,
})
@@ -69,8 +69,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
-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;
}