Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 6544c895dc 🔊(backend) update production logger
Configure probes logs as recommended by the Django dockerflow documentation.
Updating logger's config, fixed another minor issue, silenced system
warnings were still logged when calling the __lbheartbeat__ probe. These
warnings are now properly silenced.

Incoming requests are now logged in Json. Wdyt @rouja?
2024-08-29 18:22:46 +02:00
114 changed files with 3608 additions and 5788 deletions
-22
View File
@@ -1,22 +0,0 @@
name: Helmfile lint
run-name: Helmfile lint
on:
pull_request:
branches:
- 'main'
jobs:
helmfile-lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/helmfile/helmfile:latest
steps:
-
uses: numerique-gouv/action-helmfile-lint@main
with:
app-id: ${{ secrets.APP_ID }}
age-key: ${{ secrets.SOPS_PRIVATE }}
private-key: ${{ secrets.PRIVATE_KEY }}
helmfile-src: "src/helm"
repositories: "meet,secrets"
-1
View File
@@ -8,4 +8,3 @@ creation_rules:
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa #marie
- age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw #argocd
- age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9 #manuuu
- age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m # github-repo
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
set -e
HELMFILE=src/helm/helmfile.yaml
environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//')
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
echo -e "\n"
done
+1 -3
View File
@@ -13,9 +13,7 @@
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"eslint"
]
"matchPackageNames": []
}
]
}
+1 -1
Submodule secrets updated: 8ef9f4513a...f5fbc16e6e
+1 -2
View File
@@ -5,7 +5,7 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter
from core.api import get_frontend_configuration, viewsets
from core.api import viewsets
from core.authentication.urls import urlpatterns as oidc_urls
# - Main endpoints
@@ -23,7 +23,6 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
path("config/", get_frontend_configuration, name="config"),
]
),
),
+1 -7
View File
@@ -4,7 +4,6 @@ Utils functions used in the core app
# ruff: noqa:S311
import hashlib
import json
import random
from typing import Optional
@@ -25,11 +24,7 @@ def generate_color(identity: str) -> str:
range and ensure predictability.
"""
# ruff: noqa:S324
identity_hash = hashlib.sha1(identity.encode("utf-8"))
# Keep only hash's last 16 bits, collisions are not a concern
seed = int(identity_hash.hexdigest(), 16) & 0xFFFF
random.seed(seed)
random.seed(hash(identity))
hue = random.randint(0, 360)
saturation = random.randint(50, 75)
lightness = random.randint(25, 60)
@@ -52,7 +47,6 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str:
video_grants = VideoGrants(
room=room,
room_join=True,
room_admin=True,
can_update_own_metadata=True,
can_publish_sources=[
"camera",
+20 -14
View File
@@ -251,19 +251,6 @@ class Base(Configuration):
"REDOC_DIST": "SIDECAR",
}
# Frontend
FRONTEND_CONFIGURATION = {
"analytics": values.DictValue(
{}, environ_name="FRONTEND_ANALYTICS", environ_prefix=None
),
"support": values.DictValue(
{}, environ_name="FRONTEND_SUPPORT", environ_prefix=None
),
"silence_livekit_debug_logs": values.BooleanValue(
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
),
}
# Mail
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = values.Value(None)
@@ -271,7 +258,6 @@ class Base(Configuration):
EMAIL_HOST_PASSWORD = values.Value(None)
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
AUTH_USER_MODEL = "core.User"
@@ -571,6 +557,26 @@ class Production(Base):
},
}
LOGGING = {
"version": 1,
"formatters": {
"json": {"()": "dockerflow.logging.JsonLogFormatter", "logger_name": "meet"}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "json",
},
},
"loggers": {
"request.summary": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
class Feature(Production):
"""
+12 -12
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.7"
version = "0.1.4"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.19",
"boto3==1.35.5",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -36,11 +36,11 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.1.1",
"django==5.1",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"easy_thumbnails==2.9",
"factory_boy==3.3.1",
"freezegun==1.5.1",
"gunicorn==23.0.0",
@@ -48,11 +48,11 @@ dependencies = [
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.2",
"psycopg[binary]==3.2.1",
"PyJWT==2.9.0",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.14.0",
"sentry-sdk==2.13.0",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.7.0",
@@ -71,18 +71,18 @@ dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.7.1",
"ipdb==0.13.13",
"ipython==8.27.0",
"ipython==8.26.0",
"pyfakefs==5.6.0",
"pylint-django==2.5.5",
"pylint==3.2.7",
"pylint==3.2.6",
"pytest-cov==5.0.0",
"pytest-django==4.9.0",
"pytest==8.3.3",
"pytest-django==4.8.0",
"pytest==8.3.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.5",
"types-requests==2.32.0.20240914",
"ruff==0.6.2",
"types-requests==2.32.0.20240712",
]
[tool.setuptools]
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Visio</title>
<title>Meet</title>
</head>
<body>
<div id="root"></div>
+2768 -2462
View File
File diff suppressed because it is too large Load Diff
+27 -32
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.7",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,48 +13,43 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.6.0",
"@livekit/components-styles": "1.1.2",
"@livekit/track-processors": "0.3.2",
"@pandacss/preset-panda": "0.46.1",
"@react-aria/toast": "3.0.0-beta.15",
"@livekit/components-react": "2.3.3",
"@livekit/components-styles": "1.0.12",
"@pandacss/preset-panda": "0.41.0",
"@remixicon/react": "4.2.0",
"@tanstack/react-query": "5.56.2",
"crisp-sdk-web": "1.0.25",
"@tanstack/react-query": "5.49.2",
"hoofd": "1.7.1",
"i18next": "23.15.1",
"i18next": "23.12.1",
"i18next-browser-languagedetector": "8.0.0",
"i18next-parser": "9.0.2",
"i18next-parser": "9.0.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.5.3",
"posthog-js": "1.164.1",
"react": "18.3.1",
"react-aria-components": "1.3.3",
"react-dom": "18.3.1",
"react-i18next": "15.0.2",
"use-sound": "4.0.3",
"valtio": "2.0.0",
"wouter": "3.3.5"
"livekit-client": "2.3.1",
"react": "18.2.0",
"react-aria-components": "1.2.1",
"react-dom": "18.2.0",
"react-i18next": "14.1.3",
"valtio": "1.13.2",
"wouter": "3.3.0"
},
"devDependencies": {
"@pandacss/dev": "0.46.1",
"@tanstack/eslint-plugin-query": "5.57.1",
"@tanstack/react-query-devtools": "5.56.2",
"@types/node": "20.16.6",
"@types/react": "18.3.8",
"@pandacss/dev": "0.41.0",
"@tanstack/eslint-plugin-query": "5.49.1",
"@tanstack/react-query-devtools": "5.49.2",
"@types/node": "20.14.9",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "8.7.0",
"@typescript-eslint/parser": "8.7.0",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"@vitejs/plugin-react": "4.3.1",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.12",
"postcss": "8.4.47",
"eslint-plugin-react-refresh": "0.4.7",
"postcss": "8.4.39",
"prettier": "3.3.3",
"typescript": "5.6.2",
"vite": "5.4.5",
"vite-tsconfig-paths": "5.0.1"
"typescript": "5.5.2",
"vite": "5.3.1",
"vite-tsconfig-paths": "4.3.2"
}
}
-23
View File
@@ -63,29 +63,6 @@ const config: Config = {
'75%': { boxShadow: '0 0 0 30px rgba(255, 255, 255, 0)' },
'100%': { boxShadow: '0 0 0 0 rgba(255, 255, 255, 0)' },
},
active_speaker: {
'0%': { height: '4px' },
'25%': { height: '8px' },
'50%': { height: '6px' },
'100%': { height: '16px' },
},
active_speake_small: {
'0%': { height: '4px' },
'25%': { height: '6px' },
'50%': { height: '4px' },
'100%': { height: '8px' },
},
wave_hand: {
'0%': { transform: 'rotate(0deg)' },
'20%': { transform: 'rotate(-20deg)' },
'80%': { transform: 'rotate(20deg)' },
'100%': { transform: 'rotate(0)' },
},
pulse_mic: {
'0%': { color: 'primary', opacity: '1' },
'50%': { color: 'primary', opacity: '0.8' },
'100%': { color: 'primary', opacity: '1' },
},
},
tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff.
Binary file not shown.
+5 -2
View File
@@ -11,15 +11,18 @@ import { Layout } from './layout/Layout'
import { NotFoundScreen } from './components/NotFoundScreen'
import { routes } from './routes'
import './i18n/init'
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
import { queryClient } from '@/api/queryClient'
import { AppInitialization } from '@/components/AppInitialization'
function App() {
const { i18n } = useTranslation()
useLang(i18n.language)
const isProduction = import.meta.env.PROD
silenceLiveKitLogs(isProduction)
return (
<QueryClientProvider client={queryClient}>
<AppInitialization />
<Suspense fallback={null}>
<I18nProvider locale={i18n.language}>
<Layout>
-1
View File
@@ -1,5 +1,4 @@
export const keys = {
user: 'user',
room: 'room',
config: 'config',
}
-26
View File
@@ -1,26 +0,0 @@
import { fetchApi } from './fetchApi'
import { keys } from './queryKeys'
import { useQuery } from '@tanstack/react-query'
export interface ApiConfig {
analytics?: {
id: string
host: string
}
support?: {
id: string
}
silence_livekit_debug_logs?: boolean
}
const fetchConfig = (): Promise<ApiConfig> => {
return fetchApi<ApiConfig>(`config/`)
}
export const useConfig = () => {
return useQuery({
queryKey: [keys.config],
queryFn: fetchConfig,
staleTime: Infinity,
})
}
@@ -1,20 +0,0 @@
import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
export const AppInitialization = () => {
const { data } = useConfig()
const {
analytics = {},
support = {},
silence_livekit_debug_logs = false,
} = data || {}
useAnalytics(analytics)
useSupport(support)
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
}
+1 -1
View File
@@ -19,7 +19,7 @@ const avatar = cva({
list: {
width: '32px',
height: '32px',
fontSize: '1.25rem',
fontSize: '0.8rem',
},
placeholder: {
width: '100%',
File diff suppressed because one or more lines are too long
@@ -27,17 +27,12 @@ export const SoundTester = () => {
return (
<>
<Button
invisible
onPress={() => {
audioRef?.current?.play()
setIsPlaying(true)
}}
size="sm"
isDisabled={isPlaying}
fullWidth
style={{
color: isPlaying ? 'var(--colors-primary)' : undefined,
}}
>
{isPlaying ? t('audio.speakers.ongoingTest') : t('audio.speakers.test')}
</Button>
@@ -1,39 +0,0 @@
import { useEffect } from 'react'
import { useLocation } from 'wouter'
import posthog from 'posthog-js'
import { ApiUser } from '@/features/auth/api/ApiUser'
export const startAnalyticsSession = (data: ApiUser) => {
if (posthog._isIdentified()) return
const { id, email } = data
posthog.identify(id, { email })
}
export const terminateAnalyticsSession = () => {
if (!posthog._isIdentified()) return
posthog.reset()
}
export type useAnalyticsProps = {
id?: string
host?: string
}
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
const [location] = useLocation()
useEffect(() => {
if (!id || !host) return
if (posthog.__loaded) return
posthog.init(id, {
api_host: host,
person_profiles: 'always',
})
}, [id, host])
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
useEffect(() => {
posthog.capture('$pageview')
}, [location])
return null
}
@@ -20,7 +20,7 @@ export const fetchUser = (): Promise<ApiUser | false> => {
// make sure to not resolve the promise while trying to silent login
// so that consumers of fetchUser don't think the work already ended
if (canAttemptSilentLogin()) {
attemptSilentLogin(300)
attemptSilentLogin(3600)
} else {
resolve(false)
}
+2 -12
View File
@@ -2,12 +2,9 @@ import { useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import { fetchUser } from './fetchUser'
import { type ApiUser } from './ApiUser'
import { useEffect } from 'react'
import { startAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { initializeSupportSession } from '@/features/support/hooks/useSupport'
/**
* returns info about currently logged-in user
* returns info about currently logged in user
*
* `isLoggedIn` is undefined while query is loading and true/false when it's done
*/
@@ -15,16 +12,9 @@ export const useUser = () => {
const query = useQuery({
queryKey: [keys.user],
queryFn: fetchUser,
staleTime: Infinity,
staleTime: 1000 * 60 * 60, // 1 hour
})
useEffect(() => {
if (query?.data) {
startAnalyticsSession(query.data)
initializeSupportSession(query.data)
}
}, [query.data])
const isLoggedIn =
query.status === 'success' ? query.data !== false : undefined
const isLoggedOut = isLoggedIn === false
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
name="roomId"
label={t('joinInputLabel')}
description={t('joinInputExample', {
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
})}
validate={(value) => {
return !isRoomValid(value.trim()) ? (
@@ -1,9 +1,8 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Button, Dialog, type DialogProps, P, Text } from '@/primitives'
import { Div, Button, Dialog, Input, type DialogProps, P } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
// fixme - duplication with the InviteDialog
export const LaterMeetingDialog = ({
@@ -12,17 +11,19 @@ export const LaterMeetingDialog = ({
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home')
const roomUrl = getRouteUrl('room', roomId)
const [isCopied, setIsCopied] = useState(false)
const copyLabel = t('laterMeetingDialog.copy')
const copiedLabel = t('laterMeetingDialog.copied')
const [copyLinkLabel, setCopyLinkLabel] = useState(copyLabel)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
if (copyLinkLabel == copiedLabel) {
const timeout = setTimeout(() => {
setCopyLinkLabel(copyLabel)
}, 5000)
return () => {
clearTimeout(timeout)
}
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
}, [copyLinkLabel, copyLabel, copiedLabel])
return (
<Dialog
@@ -31,63 +32,31 @@ export const LaterMeetingDialog = ({
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
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
<HStack>
<div
style={{
backgroundColor: '#d9e5ff',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
}}
>
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('laterMeetingDialog.permissions')}
</Text>
<HStack alignItems="stretch" gap="gutter">
<Div flex="1">
<Input
type="text"
aria-label={t('laterMeetingDialog.inputLabel')}
value={roomUrl}
readOnly
onClick={(e) => {
e.currentTarget.select()
}}
/>
</Div>
<Div minWidth="8rem">
<Button
variant="primary"
size="sm"
fullWidth
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setCopyLinkLabel(copiedLabel)
}}
>
{copyLinkLabel}
</Button>
</Div>
</HStack>
</Dialog>
)
+11 -17
View File
@@ -1,18 +1,18 @@
import { useTranslation } from 'react-i18next'
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
import { DialogTrigger } from 'react-aria-components'
import { Button, Menu, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { Centered } from '@/layout/Centered'
import { generateRoomId } from '@/features/rooms'
import { useUser, UserAware } from '@/features/auth'
import { authUrl, useUser, UserAware } from '@/features/auth'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
import { ProConnectButton } from '@/components/ProConnectButton'
import { useCreateRoom } from '@/features/rooms'
import { usePersistentUserChoices } from '@livekit/components-react'
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import { RiAddLine, RiLink } from '@remixicon/react'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { useState } from 'react'
@@ -27,6 +27,8 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
console.log(laterRoomId)
return (
<UserAware>
<Screen>
@@ -42,12 +44,10 @@ export const Home = () => {
{t('loginToCreateMeeting')}
</Text>
)}
<HStack gap="gutter" alignItems="start">
<HStack gap="gutter">
{isLoggedIn ? (
<Menu>
<Button variant="primary" data-attr="create-meeting">
{t('createMeeting')}
</Button>
<Button variant="primary">{t('createMeeting')}</Button>
<RACMenu>
<MenuItem
className={menuItemRecipe({ icon: true })}
@@ -59,7 +59,6 @@ export const Home = () => {
})
)
}}
data-attr="create-option-instant"
>
<RiAddLine size={18} />
{t('createMenu.instantOption')}
@@ -72,7 +71,6 @@ export const Home = () => {
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
>
<RiLink size={18} />
{t('createMenu.laterOption')}
@@ -80,16 +78,12 @@ export const Home = () => {
</RACMenu>
</Menu>
) : (
<ProConnectButton />
<Button variant="primary" href={authUrl()}>
{t('login', { ns: 'global' })}
</Button>
)}
<DialogTrigger>
<Button
variant="primary"
outline
style={{
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
}}
>
<Button variant="primary" outline>
{t('joinMeeting')}
</Button>
<JoinMeetingDialog />
@@ -1,112 +0,0 @@
import { useEffect } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { Participant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
import { isMobileBrowser } from '@livekit/components-core'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
useEffect(() => {
const showJoinNotification = (participant: Participant) => {
if (isMobileBrowser()) {
return
}
triggerNotificationSound(NotificationType.Joined)
toastQueue.add(
{
participant,
type: NotificationType.Joined,
},
{
timeout: 5000,
}
)
}
room.on(RoomEvent.ParticipantConnected, showJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const removeJoinNotification = (participant: Participant) => {
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Joined
)
if (existingToast) {
toastQueue.close(existingToast.key)
}
}
room.on(RoomEvent.ParticipantDisconnected, removeJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, removeJoinNotification)
}
}, [room])
useEffect(() => {
const handleNotificationReceived = (
prevMetadataStr: string | undefined,
participant: Participant
) => {
if (!participant) return
if (isMobileBrowser()) return
if (participant.isLocal) return
const prevMetadata = JSON.parse(prevMetadataStr || '{}')
const metadata = JSON.parse(participant.metadata || '{}')
if (prevMetadata.raised == metadata.raised) return
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Raised
)
if (existingToast && prevMetadata.raised && !metadata.raised) {
toastQueue.close(existingToast.key)
return
}
if (!existingToast && !prevMetadata.raised && metadata.raised) {
triggerNotificationSound(NotificationType.Raised)
toastQueue.add(
{
participant,
type: NotificationType.Raised,
},
{ timeout: 5000 }
)
}
}
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
return () => {
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const closeAllToasts = () => {
toastQueue.visibleToasts.forEach(({ key }) => toastQueue.close(key))
}
room.on(RoomEvent.Disconnected, closeAllToasts)
return () => {
room.off(RoomEvent.Disconnected, closeAllToasts)
}
}, [room])
return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
<ToastProvider />
</Div>
)
}
@@ -1,6 +0,0 @@
export enum NotificationType {
Joined = 'joined',
Default = 'default',
Raised = 'raised',
Lowered = 'lowered',
}
@@ -1,58 +0,0 @@
import { useToast } from '@react-aria/toast'
import { Button } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { ToastState } from '@react-stately/toast'
import { styled } from '@/styled-system/jsx'
import { useRef } from 'react'
import { ToastData } from './ToastProvider'
import type { QueuedToast } from '@react-stately/toast'
export const StyledToastContainer = styled('div', {
base: {
margin: 0.5,
boxShadow:
'rgba(0, 0, 0, 0.5) 0px 4px 8px 0px, rgba(0, 0, 0, 0.3) 0px 6px 20px 4px',
backgroundColor: '#494c4f',
color: 'white',
borderRadius: '8px',
'&[data-entering]': { animation: 'fade 200ms' },
'&[data-exiting]': { animation: 'fade 150ms reverse ease-in' },
width: 'fit-content',
marginLeft: 'auto',
},
})
const StyledToast = styled('div', {
base: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '1rem',
padding: '10px',
},
})
export interface ToastProps {
key: string
toast: QueuedToast<ToastData>
state: ToastState<ToastData>
}
export function Toast({ state, ...props }: ToastProps) {
const ref = useRef(null)
const { toastProps, contentProps, closeButtonProps } = useToast(
props,
state,
ref
)
return (
<StyledToastContainer {...toastProps} ref={ref}>
<StyledToast>
<div {...contentProps}>{props.toast.content?.message} machine a</div>
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine color="white" />
</Button>
</StyledToast>
</StyledToastContainer>
)
}
@@ -1,73 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { Button as RACButton } from 'react-aria-components'
import { Track } from 'livekit-client'
import Source = Track.Source
import { useMaybeLayoutContext } from '@livekit/components-react'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack, styled } from '@/styled-system/jsx'
import { Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
const ClickableToast = styled(RACButton, {
base: {
cursor: 'pointer',
display: 'flex',
borderRadius: 'inherit',
},
})
export function ToastJoined({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const layoutContext = useMaybeLayoutContext()
const participant = props.toast.content.participant
const trackReference = {
participant,
publication: participant.getTrackPublication(Source.Camera),
source: Source.Camera,
}
const pinParticipant = () => {
layoutContext?.pin.dispatch?.({
msg: 'set_pin',
trackReference,
})
}
return (
<StyledToastContainer {...toastProps} ref={ref}>
<ClickableToast
ref={ref}
onPress={(e) => {
pinParticipant()
closeButtonProps.onPress?.(e)
}}
>
<HStack justify="center" alignItems="center" {...contentProps}>
<Div display="flex" overflow="hidden" width="128" height="72">
<ParticipantTile
trackRef={trackReference}
disableSpeakingIndicator={true}
disableMetadata={true}
style={{
borderRadius: '7px 0 0 7px',
width: '100%',
}}
/>
</Div>
<Div padding={20} {...titleProps}>
{t('joined.description', {
name: participant.name || t('defaultName'),
})}
</Div>
</HStack>
</ClickableToast>
</StyledToastContainer>
)
}
@@ -1,28 +0,0 @@
/* eslint-disable react-refresh/only-export-components */
import { ToastQueue, useToastQueue } from '@react-stately/toast'
import { ToastRegion } from './ToastRegion'
import { Participant } from 'livekit-client'
import { NotificationType } from '../NotificationType'
export interface ToastData {
participant: Participant
type: NotificationType
message?: string
}
// Using a global queue for toasts allows for centralized management and queuing of notifications
// from anywhere in the app, providing greater flexibility in complex scenarios.
export const toastQueue = new ToastQueue<ToastData>({
maxVisibleToasts: 5,
})
export const ToastProvider = ({ ...props }) => {
const state = useToastQueue<ToastData>(toastQueue)
return (
<>
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
)
}
@@ -1,66 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { Button, Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiHand } from '@remixicon/react'
import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction'
export function ToastRaised({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const participant = props.toast.content.participant
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
<RiHand
color="white"
style={{
marginRight: '1rem',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
<Div {...titleProps} marginRight={0.5}>
{t('raised.description', {
name: participant.name || t('defaultName'),
})}
</Div>
{!isParticipantsOpen && (
<Button
size="sm"
variant="text"
style={{
color: '#60a5fa',
}}
onPress={(e) => {
toggleParticipants()
closeButtonProps.onPress?.(e)
}}
>
{t('raised.cta')}
</Button>
)}
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine size={18} color="white" />
</Button>
</HStack>
</StyledToastContainer>
)
}
@@ -1,30 +0,0 @@
import { AriaToastRegionProps, useToastRegion } from '@react-aria/toast'
import type { ToastState } from '@react-stately/toast'
import { Toast } from './Toast'
import { useRef } from 'react'
import { NotificationType } from '../NotificationType'
import { ToastJoined } from './ToastJoined'
import { ToastData } from './ToastProvider'
import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
}
export function ToastRegion({ state, ...props }: ToastRegionProps) {
const ref = useRef(null)
const { regionProps } = useToastRegion(props, state, ref)
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => {
if (toast.content?.type === NotificationType.Joined) {
return <ToastJoined key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.Raised) {
return <ToastRaised key={toast.key} toast={toast} state={state} />
}
return <Toast key={toast.key} toast={toast} state={state} />
})}
</div>
)
}
@@ -1,18 +0,0 @@
import useSound from 'use-sound'
// fixme - handle dynamic audio output changes
export const useNotificationSound = () => {
const [play] = useSound('./sounds/notifications.mp3', {
sprite: {
joined: [0, 1150],
raised: [1400, 180],
message: [1580, 300],
waiting: [2039, 710],
success: [2740, 1304],
},
})
const triggerNotificationSound = (type: string) => {
play({ id: type })
}
return { triggerNotificationSound }
}
@@ -1,70 +0,0 @@
import { styled } from '@/styled-system/jsx'
const StyledContainer = styled('div', {
base: {
width: '24px',
height: '24px',
boxSizing: 'border-box',
backgroundColor: 'primary',
borderRadius: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '2px',
},
})
const StyledChild = styled('div', {
base: {
backgroundColor: 'white',
width: '4px',
height: '4px',
borderRadius: '4px',
animationDuration: '400ms',
animationName: 'active_speaker',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
},
variants: {
active: {
true: {
animationIterationCount: 'infinite',
},
false: {
animationIterationCount: 0,
},
},
size: {
small: {
animationName: 'active_speake_small',
},
},
},
})
export const ActiveSpeaker = ({ isSpeaking }: { isSpeaking: boolean }) => {
return (
<StyledContainer>
<StyledChild
active={isSpeaking}
size="small"
style={{
animationDelay: '300ms',
}}
/>
<StyledChild
active={isSpeaking}
style={{
animationDelay: '100ms',
}}
/>
<StyledChild
active={isSpeaking}
size="small"
style={{
animationDelay: '500ms',
}}
/>
</StyledContainer>
)
}
@@ -9,6 +9,7 @@ import {
import { Room, RoomOptions } from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { QueryAware } from '@/components/QueryAware'
import { ErrorScreen } from '@/components/ErrorScreen'
@@ -18,7 +19,6 @@ import { useCreateRoom } from '../api/createRoom'
import { InviteDialog } from './InviteDialog'
import { VideoConference } from '../livekit/prefabs/VideoConference'
import posthog from 'posthog-js'
export const Conference = ({
roomId,
@@ -31,9 +31,6 @@ export const Conference = ({
mode?: 'join' | 'create'
initialRoomData?: ApiRoom
}) => {
useEffect(() => {
posthog.capture('visit-room', { slug: roomId })
}, [roomId])
const fetchKey = [keys.room, roomId, userConfig.username]
const {
@@ -53,6 +50,7 @@ export const Conference = ({
} = useQuery({
queryKey: fetchKey,
staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation
enabled: !initialRoomData,
initialData: initialRoomData,
queryFn: () =>
fetchRoom({
@@ -82,6 +80,25 @@ export const Conference = ({
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
/**
* checks for actual click on the leave button instead of
* relying on LiveKitRoom onDisconnected because onDisconnected
* triggers even on page reload, it's not a user "onLeave" event really.
* Here we want to react to the user actually deciding to leave.
*/
useEffect(() => {
const checkOnLeaveClick = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.classList.contains('lk-disconnect-button')) {
navigateTo('feedback')
}
}
document.body.addEventListener('click', checkOnLeaveClick)
return () => {
document.body.removeEventListener('click', checkOnLeaveClick)
}
}, [])
const { t } = useTranslation('rooms')
if (isCreateError) {
// this error screen should be replaced by a proper waiting room for anonymous user.
@@ -1,37 +1,8 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
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'
import {
RiCheckLine,
RiCloseLine,
RiFileCopyLine,
RiSpam2Fill,
} from '@remixicon/react'
import { useEffect, useState } from 'react'
// fixme - extract in a proper primitive this dialog without overlay
const StyledRACDialog = styled(Dialog, {
base: {
position: 'fixed',
left: '0.75rem',
bottom: 80,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
width: '24.5rem',
borderRadius: '8px',
padding: '1.5rem',
boxShadow:
'0 1px 2px 0 rgba(60, 64, 67, .3), 0 2px 6px 2px rgba(60, 64, 67, .15)',
backgroundColor: 'white',
'&[data-entering]': { animation: 'fade 200ms' },
'&[data-exiting]': { animation: 'fade 150ms reverse ease-in' },
},
})
import { Div, Button, Dialog, Input, type DialogProps } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
export const InviteDialog = ({
roomId,
@@ -40,103 +11,48 @@ export const InviteDialog = ({
const { t } = useTranslation('rooms')
const roomUrl = getRouteUrl('room', roomId)
const [isCopied, setIsCopied] = useState(false)
const copyLabel = t('shareDialog.copy')
const copiedLabel = t('shareDialog.copied')
const [copyLinkLabel, setCopyLinkLabel] = useState(copyLabel)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
if (copyLinkLabel == copiedLabel) {
const timeout = setTimeout(() => {
setCopyLinkLabel(copyLabel)
}, 5000)
return () => {
clearTimeout(timeout)
}
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
}, [copyLinkLabel, copyLabel, copiedLabel])
return (
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VStack
alignItems="left"
justify="start"
gap={0}
style={{ maxWidth: '100%', overflow: 'hidden' }}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
{t('shareDialog.heading')}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={() => {
dialogProps.onClose?.()
close()
}}
aria-label={t('closeDialog')}
>
<RiCloseLine />
</Button>
</Div>
<P>{t('shareDialog.description')}</P>
<Dialog {...dialogProps} title={t('shareDialog.heading')}>
<HStack alignItems="stretch" gap="gutter">
<Div flex="1">
<Input
type="text"
aria-label={t('shareDialog.inputLabel')}
value={roomUrl}
readOnly
onClick={(e) => {
e.currentTarget.select()
}}
/>
</Div>
<Div minWidth="8rem">
<Button
variant={isCopied ? 'success' : 'primary'}
variant="primary"
size="sm"
fullWidth
aria-label={t('shareDialog.copy')}
style={{
justifyContent: 'start',
}}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
setCopyLinkLabel(copiedLabel)
}}
onHoverChange={setIsHovered}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('shareDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('shareDialog.copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
{copyLinkLabel}
</Button>
<HStack>
<div
style={{
backgroundColor: '#d9e5ff',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
}}
>
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('shareDialog.permissions')}
</Text>
</HStack>
</VStack>
)}
</StyledRACDialog>
</Div>
</HStack>
</Dialog>
)
}
@@ -16,8 +16,8 @@ export const Join = ({
<PreJoin
persistUserChoices
onSubmit={onSubmit}
micLabel={t('join.audioinput.label')}
camLabel={t('join.videoinput.label')}
micLabel={t('join.micLabel')}
camLabel={t('join.camlabel')}
joinLabel={t('join.joinLabel')}
userLabel={t('join.userLabel')}
/>
@@ -1,5 +0,0 @@
export const buildServerApiUrl = (origin: string, path: string) => {
const sanitizedOrigin = origin.replace(/\/$/, '')
const sanitizedPath = path.replace(/^\//, '')
return `${sanitizedOrigin}/${sanitizedPath}`
}
@@ -1,21 +0,0 @@
import { ApiError } from '@/api/ApiError'
export const fetchServerApi = async <T = Record<string, unknown>>(
url: string,
token: string,
options?: RequestInit
): Promise<T> => {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options?.headers,
},
})
const result = await response.json()
if (!response.ok) {
throw new ApiError(response.status, result)
}
return result
}
@@ -1,33 +0,0 @@
import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
export const useLowerHandParticipant = () => {
const data = useRoomData()
const lowerHandParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newMetadata = JSON.parse(participant.metadata || '{}')
newMetadata.raised = !newMetadata.raised
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/UpdateParticipant'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
metadata: JSON.stringify(newMetadata),
permission: participant.permissions,
}),
}
)
}
return { lowerHandParticipant }
}
@@ -1,19 +0,0 @@
import { Participant } from 'livekit-client'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant'
export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant()
const lowerHandParticipants = (participants: Array<Participant>) => {
try {
const promises = participants.map((participant) =>
lowerHandParticipant(participant)
)
return Promise.all(promises)
} catch (error) {
console.error('An error occurred while lowering hands :', error)
throw new Error('An error occurred while lowering hands.')
}
}
return { lowerHandParticipants }
}
@@ -1,39 +0,0 @@
import { Participant, Track } from 'livekit-client'
import Source = Track.Source
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
export const useMuteParticipant = () => {
const data = useRoomData()
const muteParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const trackSid = participant.getTrackPublication(
Source.Microphone
)?.trackSid
if (!trackSid) {
throw new Error('Missing audio track')
}
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/MutePublishedTrack'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
muted: true,
track_sid: trackSid,
}),
}
)
}
return { muteParticipant }
}
@@ -1,191 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { Text, P, ToggleButton, Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import {
BackgroundBlur,
BackgroundOptions,
ProcessorWrapper,
BackgroundTransformer,
} from '@livekit/track-processors'
const Information = styled('div', {
base: {
backgroundColor: 'orange.50',
borderRadius: '4px',
padding: '0.75rem 0.75rem',
marginTop: '0.8rem',
alignItems: 'start',
},
})
enum BlurRadius {
NONE = 0,
LIGHT = 5,
NORMAL = 10,
}
export const Effects = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const { isCameraEnabled, cameraTrack, localParticipant } =
useLocalParticipant()
const videoRef = useRef<HTMLVideoElement>(null)
const [processorPending, setProcessorPending] = useState(false)
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
const getProcessor = () => {
return localCameraTrack?.getProcessor() as ProcessorWrapper<BackgroundOptions>
}
const getBlurRadius = (): BlurRadius => {
const processor = getProcessor()
return (
(processor?.transformer as BackgroundTransformer)?.options?.blurRadius ||
BlurRadius.NONE
)
}
const toggleBlur = async (blurRadius: number) => {
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true)
if (!localCameraTrack) return
setProcessorPending(true)
const processor = getProcessor()
const currentBlurRadius = getBlurRadius()
try {
if (blurRadius == currentBlurRadius && processor) {
await localCameraTrack.stopProcessor()
} else if (!processor) {
await localCameraTrack.setProcessor(BackgroundBlur(blurRadius))
} else {
await processor?.updateTransformerOptions({ blurRadius })
}
} catch (error) {
console.error('Error applying blur:', error)
} finally {
setProcessorPending(false)
}
}
useEffect(() => {
const videoElement = videoRef.current
if (!videoElement) return
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement)
attachVideoTrack()
return () => {
if (!videoElement) return
localCameraTrack.detach(videoElement)
}
}, [localCameraTrack, isCameraEnabled])
const isSelected = (blurRadius: BlurRadius) => {
return isCameraEnabled && getBlurRadius() == blurRadius
}
const tooltipLabel = (blurRadius: BlurRadius) => {
return t(`blur.${isSelected(blurRadius) ? 'clear' : 'apply'}`)
}
return (
<VStack padding="0 1.5rem">
{localCameraTrack && isCameraEnabled ? (
<video
ref={videoRef}
width="100%"
muted
style={{
transform: 'rotateY(180deg)',
minHeight: '175px',
borderRadius: '8px',
}}
/>
) : (
<div
style={{
width: '100%',
height: '174px',
display: 'flex',
backgroundColor: 'black',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<P
style={{
color: 'white',
textAlign: 'center',
textWrap: 'balance',
marginBottom: 0,
}}
>
{t('activateCamera')}
</P>
</div>
)}
<Div
alignItems={'left'}
width={'100%'}
style={{
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
padding: '0.5rem 1rem',
}}
>
<H
lvl={3}
style={{
marginBottom: '0.4rem',
}}
>
{t('heading')}
</H>
{ProcessorWrapper.isSupported ? (
<HStack>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.LIGHT)}
tooltip={tooltipLabel(BlurRadius.LIGHT)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.LIGHT)}
isSelected={isSelected(BlurRadius.LIGHT)}
>
{t('blur.light')}
</ToggleButton>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.NORMAL)}
tooltip={tooltipLabel(BlurRadius.NORMAL)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.NORMAL)}
isSelected={isSelected(BlurRadius.NORMAL)}
>
{t('blur.normal')}
</ToggleButton>
</HStack>
) : (
<Text variant="sm">{t('notAvailable')}</Text>
)}
<Information>
<Text
variant="sm"
style={{
textWrap: 'balance',
}}
>
{t('experimental')}
</Text>
</Information>
</Div>
</VStack>
)
}
@@ -1,26 +0,0 @@
import { useTrackMutedIndicator } from '@livekit/components-react'
import { Participant, Track } from 'livekit-client'
import Source = Track.Source
import { Div } from '@/primitives'
import { RiMicOffFill } from '@remixicon/react'
export const MutedMicIndicator = ({
participant,
}: {
participant: Participant
}) => {
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Source.Microphone,
})
if (!isMuted) {
return null
}
return (
<Div padding={0.25} backgroundColor="red" borderRadius="4px">
<RiMicOffFill size={16} color="white" />
</Div>
)
}
@@ -6,6 +6,7 @@ import {
ParticipantName,
ParticipantTileProps,
ScreenShareIcon,
TrackMutedIndicator,
useEnsureTrackRef,
useFeatureContext,
useIsEncrypted,
@@ -24,10 +25,6 @@ import {
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
import { RiHand } from '@remixicon/react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from '@/features/rooms/livekit/components/MutedMicIndicator'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -44,24 +41,19 @@ export function TrackRefContextIfNeeded(
)
}
interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean
}
export const ParticipantTile: (
props: ParticipantTileExtendedProps & React.RefAttributes<HTMLDivElement>
props: ParticipantTileProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
ParticipantTileExtendedProps
ParticipantTileProps
>(function ParticipantTile(
{
trackRef,
children,
onParticipantClick,
disableSpeakingIndicator,
disableMetadata,
...htmlProps
}: ParticipantTileExtendedProps,
}: ParticipantTileProps,
ref
) {
const trackReference = useEnsureTrackRef(trackRef)
@@ -92,10 +84,6 @@ export const ParticipantTile: (
[trackReference, layoutContext]
)
const { isHandRaised } = useRaisedHand({
participant: trackReference.participant,
})
return (
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
<TrackRefContextIfNeeded trackRef={trackReference}>
@@ -124,56 +112,34 @@ export const ParticipantTile: (
participant={trackReference.participant}
/>
</div>
{!disableMetadata && (
<div className="lk-participant-metadata">
<HStack gap={0.25}>
<MutedMicIndicator
participant={trackReference.participant}
/>
<div
className="lk-participant-metadata-item"
style={{
minHeight: '24px',
backgroundColor: isHandRaised ? 'white' : undefined,
color: isHandRaised ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{trackReference.source === Track.Source.Camera ? (
<>
{isHandRaised && (
<RiHand
color="black"
size={16}
style={{
marginInlineEnd: '.25rem', // fixme - match TrackMutedIndicator styling
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
)}
{isEncrypted && (
<LockLockedIcon
style={{ marginRight: '0.25rem' }}
/>
)}
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
<div className="lk-participant-metadata">
<div className="lk-participant-metadata-item">
{trackReference.source === Track.Source.Camera ? (
<>
{isEncrypted && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
)}
</div>
</HStack>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
<TrackMutedIndicator
trackRef={{
participant: trackReference.participant,
source: Track.Source.Microphone,
}}
show={'muted'}
></TrackMutedIndicator>
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
)}
</div>
)}
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
</>
)}
{!disableMetadata && <FocusToggle trackRef={trackReference} />}
<FocusToggle trackRef={trackReference} />
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
</div>
@@ -1,88 +0,0 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Box, Button, Div } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useWidgetInteraction } from '../hooks/useWidgetInteraction'
import { ReactNode } from 'react'
import { Effects } from './Effects'
type StyledSidePanelProps = {
title: string
children: ReactNode
onClose: () => void
closeButtonTooltip: string
}
const StyledSidePanel = ({
title,
children,
onClose,
closeButtonTooltip,
}: StyledSidePanelProps) => (
<Box
size="sm"
minWidth="360px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0,
gap: 0,
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
>
{title}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">{children}</Div>
</Box>
)
export const SidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const { isParticipantsOpen, isEffectsOpen } = useWidgetInteraction()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
if (!sidePanel) {
return
}
return (
<StyledSidePanel
title={t(`heading.${sidePanel}`)}
onClose={() => (layoutStore.sidePanel = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${sidePanel}`),
})}
>
{isParticipantsOpen && <ParticipantsList />}
{isEffectsOpen && <Effects />}
</StyledSidePanel>
)
}
@@ -2,13 +2,18 @@ import { useTranslation } from 'react-i18next'
import { RiChat1Line } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
export const ChatToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
const { t } = useTranslation('rooms')
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
const tooltipLabel = isChatOpen ? 'open' : 'closed'
const { dispatch, state } = useLayoutContext().widget
const tooltipLabel = state?.showChat ? 'open' : 'closed'
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
return (
<div
@@ -20,15 +25,17 @@ export const ChatToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isChatOpen}
onPress={() => toggleChat()}
data-attr={`controls-chat-${tooltipLabel}`}
aria-label={t(`controls.chat.${tooltipLabel}`)}
tooltip={t(`controls.chat.${tooltipLabel}`)}
isSelected={state?.showChat}
onPress={() => {
if (showParticipants) participantsStore.showParticipants = false
if (dispatch) dispatch({ msg: 'toggle_chat' })
}}
>
<RiChat1Line />
</ToggleButton>
{!!unreadMessages && (
{!!state?.unreadMessages && (
<div
className={css({
position: 'absolute',
@@ -1,38 +0,0 @@
import { useTranslation } from 'react-i18next'
import { RiHand } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
export const HandToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
return (
<div
className={css({
position: 'relative',
display: 'inline-block',
})}
>
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isHandRaised}
onPress={() => toggleRaisedHand()}
data-attr={`controls-hand-${tooltipLabel}`}
>
<RiHand />
</ToggleButton>
</div>
)
}
@@ -1,37 +0,0 @@
import { useConnectionState, useRoomContext } from '@livekit/components-react'
import { Button } from '@/primitives'
import { navigateTo } from '@/navigation/navigateTo'
import { RiPhoneFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ConnectionState } from 'livekit-client'
export const LeaveButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const room = useRoomContext()
const connectionState = useConnectionState(room)
return (
<Button
isDisabled={connectionState === ConnectionState.Disconnected}
variant={'danger'}
tooltip={t('leave')}
aria-label={t('leave')}
onPress={() => {
room
.disconnect(true)
.catch((e) =>
console.error('An error occurred while disconnecting:', e)
)
.finally(() => {
navigateTo('feedback')
})
}}
data-attr="controls-leave"
>
<RiPhoneFill
style={{
transform: 'rotate(135deg)',
}}
/>
</Button>
)
}
@@ -1,15 +1,13 @@
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import {
RiAccountBoxLine,
RiFeedbackLine,
RiQuestionLine,
RiSettings3Line,
} from '@remixicon/react'
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction } from 'react'
import { DialogState } from './OptionsButton'
import { Separator } from '@/primitives/Separator'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
@@ -17,8 +15,8 @@ export const OptionsMenuItems = ({
}: {
onOpenDialog: Dispatch<SetStateAction<DialogState>>
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useWidgetInteraction()
const { t } = useTranslation('rooms')
return (
<RACMenu
style={{
@@ -26,33 +24,29 @@ export const OptionsMenuItems = ({
width: '300px',
}}
>
<Section>
<MenuItem
onAction={() => toggleEffects()}
className={menuItemRecipe({ icon: true })}
>
<RiAccountBoxLine size={20} />
{t('effects')}
</MenuItem>
</Section>
<Separator />
<Section>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={20} />
{t('feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={20} />
{t('settings')}
</MenuItem>
</Section>
<MenuItem
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiQuestionLine size={18} />
{t('options.items.support')}
</MenuItem>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={18} />
{t('options.items.feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={18} />
{t('options.items.settings')}
</MenuItem>
</RACMenu>
)
}
@@ -1,82 +0,0 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'
import { Button } from '@/primitives'
type HandRaisedListItemProps = {
participant: Participant
}
export const HandRaisedListItem = ({
participant,
}: HandRaisedListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
const { lowerHandParticipant } = useLowerHandParticipant()
return (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<Button
square
invisible
size="sm"
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
data-attr="participants-lower-hand"
>
<RiHand />
</Button>
</HStack>
)
}
@@ -1,27 +0,0 @@
import { Button } from '@/primitives'
import { useLowerHandParticipants } from '@/features/rooms/livekit/api/lowerHandParticipants'
import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client'
type LowerAllHandsButtonProps = {
participants: Array<Participant>
}
export const LowerAllHandsButton = ({
participants,
}: LowerAllHandsButtonProps) => {
const { lowerHandParticipants } = useLowerHandParticipants()
const { t } = useTranslation('rooms')
return (
<Button
aria-label={t('participants.lowerParticipantsHand')}
size="sm"
fullWidth
variant="text"
onPress={() => lowerHandParticipants(participants)}
data-attr="participants-lower-hands"
>
{t('participants.lowerParticipantsHand')}
</Button>
)
}
@@ -1,163 +0,0 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant, Track } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import {
useIsSpeaking,
useTrackMutedIndicator,
} from '@livekit/components-react'
import Source = Track.Source
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { Button, Dialog, P } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
const MuteAlertDialog = ({
isOpen,
onClose,
onSubmit,
name,
}: {
isOpen: boolean
onClose: () => void
onSubmit: () => void
name: string
}) => {
const { t } = useTranslation('rooms')
return (
<Dialog isOpen={isOpen} role="alertdialog">
<P>{t('participants.muteParticipantAlert.description', { name })}</P>
<HStack gap={1}>
<Button variant="text" size="sm" onPress={onClose}>
{t('participants.muteParticipantAlert.cancel')}
</Button>
<Button variant="text" size="sm" onPress={onSubmit}>
{t('participants.muteParticipantAlert.confirm')}
</Button>
</HStack>
</Dialog>
)
}
type MicIndicatorProps = {
participant: Participant
}
const MicIndicator = ({ participant }: MicIndicatorProps) => {
const { t } = useTranslation('rooms')
const { muteParticipant } = useMuteParticipant()
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Source.Microphone,
})
const isSpeaking = useIsSpeaking(participant)
const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity
return (
<>
<Button
square
invisible
size="sm"
tooltip={
isLocal(participant)
? t('participants.muteYourself')
: t('participants.muteParticipant', {
name,
})
}
isDisabled={isMuted}
onPress={() =>
!isMuted && isLocal(participant)
? muteParticipant(participant)
: setIsAlertOpen(true)
}
data-attr="participants-mute"
>
{isMuted ? (
<RiMicOffFill color={'gray'} />
) : (
<RiMicFill
style={{
animation: isSpeaking ? 'pulse_mic 800ms infinite' : undefined,
}}
/>
)}
</Button>
<MuteAlertDialog
isOpen={isAlertOpen}
onSubmit={() =>
muteParticipant(participant).then(() => setIsAlertOpen(false))
}
onClose={() => setIsAlertOpen(false)}
name={name}
/>
</>
)
}
type ParticipantListItemProps = {
participant: Participant
}
export const ParticipantListItem = ({
participant,
}: ParticipantListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
return (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<HStack>
<MicIndicator participant={participant} />
</HStack>
</HStack>
)
}
@@ -1,109 +0,0 @@
import { useState } from 'react'
import { css } from '@/styled-system/css'
import { ToggleButton } from 'react-aria-components'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { RiArrowUpSLine } from '@remixicon/react'
import { Participant } from 'livekit-client'
import { useTranslation } from 'react-i18next'
const ToggleHeader = styled(ToggleButton, {
base: {
minHeight: '40px', //fixme hardcoded value
paddingRight: '.5rem',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
alignItems: 'center',
transition: 'background 200ms',
borderTopRadius: '7px',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
},
})
const Container = styled('div', {
base: {
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
},
})
const ListContainer = styled(VStack, {
base: {
borderTop: '1px solid #dadce0',
alignItems: 'start',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
paddingY: '0.5rem',
paddingX: '1rem',
gap: 0,
},
})
type ParticipantsCollapsableListProps = {
heading: string
participants: Array<Participant>
renderParticipant: (participant: Participant) => JSX.Element
action?: () => JSX.Element
}
export const ParticipantsCollapsableList = ({
heading,
participants,
renderParticipant,
action,
}: ParticipantsCollapsableListProps) => {
const { t } = useTranslation('rooms')
const [isOpen, setIsOpen] = useState(true)
const label = t(`participants.collapsable.${isOpen ? 'close' : 'open'}`, {
name: heading,
})
return (
<Container>
<ToggleHeader
isSelected={isOpen}
aria-label={label}
onPress={() => setIsOpen(!isOpen)}
style={{
borderRadius: !isOpen ? '7px' : undefined,
}}
>
<HStack
justify="space-between"
className={css({
margin: '0 1.25rem',
width: '100%',
})}
>
<div
className={css({
fontSize: '1rem',
})}
>
{heading}
</div>
<div>{participants?.length || 0}</div>
</HStack>
<RiArrowUpSLine
size={32}
style={{
transform: isOpen ? 'rotate(-180deg)' : undefined,
transition: 'transform 200ms',
}}
/>
</ToggleHeader>
{isOpen && (
<ListContainer>
{action && action()}
{participants.map((participant) => renderParticipant(participant))}
</ListContainer>
)}
</Container>
)
}
@@ -1,83 +1,136 @@
import { css } from '@/styled-system/css'
import { allParticipantRoomEvents } from '@livekit/components-core'
import { useParticipants } from '@livekit/components-react'
import { Div, H } from '@/primitives'
import { Heading } from 'react-aria-components'
import { Box, Button, Div } from '@/primitives'
import { HStack, VStack } from '@/styled-system/jsx'
import { Text, text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
import { capitalize } from '@/utils/capitalize'
import { participantsStore } from '@/stores/participants'
import { useTranslation } from 'react-i18next'
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
import { RoomEvent } from 'livekit-client'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
const { t } = useTranslation('rooms')
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
// because the 'useLocalParticipant' hook does not update the participant's information when their
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
const participants = useParticipants({
updateOnlyOn: [
RoomEvent.ParticipantNameChanged,
...allParticipantRoomEvents,
],
updateOnlyOn: allParticipantRoomEvents,
})
const sortedRemoteParticipants = participants
.slice(1)
.sort((participantA, participantB) => {
const nameA = participantA.name || participantA.identity
const nameB = participantB.name || participantB.identity
return nameA.localeCompare(nameB)
})
const formattedParticipants = participants.map((participant) => ({
name: participant.name || participant.identity,
id: participant.identity,
color: getParticipantColor(participant),
}))
const sortedParticipants = [
participants[0], // first participant returned by the hook, is always the local one
const sortedRemoteParticipants = formattedParticipants
.slice(1)
.sort((a, b) => a.name.localeCompare(b.name))
const allParticipants = [
formattedParticipants[0], // first participant returned by the hook, is always the local one
...sortedRemoteParticipants,
]
const raisedHandParticipants = participants.filter((participant) => {
const data = JSON.parse(participant.metadata || '{}')
return data.raised
})
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
<>
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
marginBottom: '0.83em',
})}
>
{t('subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<Div marginBottom=".9375rem">
<ParticipantsCollapsableList
heading={t('raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
action={() => (
<LowerAllHandsButton participants={raisedHandParticipants} />
)}
/>
</Div>
<Box
size="sm"
minWidth="300px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
})}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
<span>{t('participants.heading')}</span>{' '}
<span
className={css({
marginLeft: '0.75rem',
fontWeight: 'normal',
fontSize: '1rem',
})}
>
{participants?.length}
</span>
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={() => (participantsStore.showParticipants = false)}
aria-label={t('participants.closeButton')}
tooltip={t('participants.closeButton')}
>
<RiCloseLine />
</Button>
</Div>
{participants?.length > 0 && (
<VStack
role="list"
className={css({
alignItems: 'start',
gap: 'none',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
})}
>
{allParticipants.map((participant, index) => (
<HStack
role="listitem"
key={participant.id}
id={participant.id}
className={css({
padding: '0.25rem 0',
})}
>
<Avatar name={participant.name} bgColor={participant.color} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{capitalize(participant.name)}
</span>
{index === 0 && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
))}
</VStack>
)}
<ParticipantsCollapsableList
heading={t('contributors')}
participants={sortedParticipants}
renderParticipant={(participant) => (
<ParticipantListItem participant={participant} />
)}
/>
</>
</Box>
)
}
@@ -2,11 +2,14 @@ import { useTranslation } from 'react-i18next'
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
import { useLayoutContext, useParticipants } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
export const ParticipantsToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
const { t } = useTranslation('rooms')
const { dispatch, state } = useLayoutContext().widget
/**
* Context could not be used due to inconsistent refresh behavior.
@@ -16,9 +19,10 @@ export const ParticipantsToggle = () => {
const participants = useParticipants()
const numParticipants = participants?.length
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
const tooltipLabel = showParticipants ? 'open' : 'closed'
return (
<div
@@ -30,11 +34,13 @@ export const ParticipantsToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isParticipantsOpen}
onPress={() => toggleParticipants()}
data-attr={`controls-participants-${tooltipLabel}`}
aria-label={t(`controls.participants.${tooltipLabel}`)}
tooltip={t(`controls.participants.${tooltipLabel}`)}
isSelected={showParticipants}
onPress={() => {
if (dispatch && state?.showChat) dispatch({ msg: 'toggle_chat' })
participantsStore.showParticipants = !showParticipants
}}
>
<RiGroupLine />
</ToggleButton>
@@ -1,56 +0,0 @@
import { Div, ToggleButton } from '@/primitives'
import { RiArrowUpLine, RiCloseFill, RiRectangleLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Track } from 'livekit-client'
import React from 'react'
export const ScreenShareToggle = (
props: Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
'source' | 'captureOptions'
>
) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
const { buttonProps, enabled } = useTrackToggle({
...props,
source: Track.Source.ScreenShare,
captureOptions: { audio: true, selfBrowserSurface: 'include' },
})
const tooltipLabel = enabled ? 'stop' : 'start'
const Icon = enabled ? RiCloseFill : RiArrowUpLine
// fixme - remove ToggleButton custom styles when we design a proper icon
return (
<ToggleButton
isSelected={enabled}
square
legacyStyle
tooltip={t(tooltipLabel)}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
)
}
style={{
maxWidth: '46px',
maxHeight: '46px',
}}
data-attr={`controls-screenshare-${tooltipLabel}`}
>
<Div position="relative">
<RiRectangleLine size={28} />
<Icon
size={16}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
</Div>
</ToggleButton>
)
}
@@ -1,120 +0,0 @@
import { useTranslation } from 'react-i18next'
import {
useMediaDeviceSelect,
useTrackToggle,
UseTrackToggleProps,
} from '@livekit/components-react'
import { HStack } from '@/styled-system/jsx'
import { Button, Menu, MenuList, ToggleButton } from '@/primitives'
import {
RemixiconComponentType,
RiArrowDownSLine,
RiMicLine,
RiMicOffLine,
RiVideoOffLine,
RiVideoOnLine,
} from '@remixicon/react'
import { Track } from 'livekit-client'
import React from 'react'
export type ToggleSource = Exclude<
Track.Source,
Track.Source.ScreenShareAudio | Track.Source.Unknown
>
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
type SelectToggleDeviceConfig = {
kind: MediaDeviceKind
iconOn: RemixiconComponentType
iconOff: RemixiconComponentType
}
type SelectToggleDeviceConfigMap = {
[key in SelectToggleSource]: SelectToggleDeviceConfig
}
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
[Track.Source.Microphone]: {
kind: 'audioinput',
iconOn: RiMicLine,
iconOff: RiMicOffLine,
},
[Track.Source.Camera]: {
kind: 'videoinput',
iconOn: RiVideoOnLine,
iconOff: RiVideoOffLine,
},
}
type SelectToggleDeviceProps<T extends ToggleSource> =
UseTrackToggleProps<T> & {
onActiveDeviceChange: (deviceId: string) => void
source: SelectToggleSource
}
export const SelectToggleDevice = <T extends ToggleSource>({
onActiveDeviceChange,
...props
}: SelectToggleDeviceProps<T>) => {
const config = selectToggleDeviceConfig[props.source]
if (!config) {
throw new Error('Invalid source')
}
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { buttonProps, enabled } = useTrackToggle(props)
const { kind, iconOn, iconOff } = config
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind })
const toggleLabel = t(enabled ? 'disable' : 'enable', {
keyPrefix: `join.${kind}`,
})
const selectLabel = t('choose', { keyPrefix: `join.${kind}` })
const Icon = enabled ? iconOn : iconOff
return (
<HStack gap={0}>
<ToggleButton
isSelected={enabled}
variant={enabled ? undefined : 'danger'}
toggledStyles={false}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement>
)
}
aria-label={toggleLabel}
tooltip={toggleLabel}
groupPosition="left"
>
<Icon />
</ToggleButton>
<Menu>
<Button
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
>
<RiArrowDownSLine />
</Button>
<MenuList
items={devices.map((d) => ({
value: d.deviceId,
label: d.label,
}))}
selectedItem={activeDeviceId}
onAction={(value) => {
setActiveMediaDevice(value as string)
onActiveDeviceChange(value as string)
}}
/>
</Menu>
</HStack>
)
}
@@ -0,0 +1,33 @@
import { RoomEvent } from 'livekit-client'
// Issue: 'allRemoteParticipantRoomEvents' is not exposed or importable. One event is missing
// to trigger the real-time update of participants when they change their name.
// This code is duplicated from LiveKit.
export const allRemoteParticipantRoomEvents = [
RoomEvent.ConnectionStateChanged,
RoomEvent.RoomMetadataChanged,
RoomEvent.ActiveSpeakersChanged,
RoomEvent.ConnectionQualityChanged,
RoomEvent.ParticipantConnected,
RoomEvent.ParticipantDisconnected,
RoomEvent.ParticipantPermissionsChanged,
RoomEvent.ParticipantMetadataChanged,
RoomEvent.ParticipantNameChanged, // This element is missing in LiveKit and causes problems
RoomEvent.TrackMuted,
RoomEvent.TrackUnmuted,
RoomEvent.TrackPublished,
RoomEvent.TrackUnpublished,
RoomEvent.TrackStreamStateChanged,
RoomEvent.TrackSubscriptionFailed,
RoomEvent.TrackSubscriptionPermissionChanged,
RoomEvent.TrackSubscriptionStatusChanged,
]
export const allParticipantRoomEvents = [
...allRemoteParticipantRoomEvents,
RoomEvent.LocalTrackPublished,
RoomEvent.LocalTrackUnpublished,
]
@@ -1,23 +0,0 @@
import { LocalParticipant, Participant } from 'livekit-client'
import { useParticipantInfo } from '@livekit/components-react'
import { isLocal } from '@/utils/livekit'
type useRaisedHandProps = {
participant: Participant
}
export function useRaisedHand({ participant }: useRaisedHandProps) {
// fixme - refactor this part to rely on attributes
const { metadata } = useParticipantInfo({ participant })
const parsedMetadata = JSON.parse(metadata || '{}')
const toggleRaisedHand = () => {
if (isLocal(participant)) {
parsedMetadata.raised = !parsedMetadata.raised
const localParticipant = participant as LocalParticipant
localParticipant.setMetadata(JSON.stringify(parsedMetadata))
}
}
return { isHandRaised: parsedMetadata.raised, toggleRaisedHand }
}
@@ -1,12 +0,0 @@
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { useRoomContext } from '@livekit/components-react'
import { useParams } from 'wouter'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
export const useRoomData = (): ApiRoom | undefined => {
const room = useRoomContext()
const { roomId } = useParams()
const queryKey = [keys.room, roomId, room.localParticipant.name]
return queryClient.getQueryData<ApiRoom>(queryKey)
}
@@ -1,46 +0,0 @@
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == 'participants'
const isEffectsOpen = sidePanel == 'effects'
const toggleParticipants = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
}
const toggleChat = () => {
if (isParticipantsOpen || isEffectsOpen) {
layoutStore.sidePanel = null
}
if (dispatch) {
dispatch({ msg: 'toggle_chat' })
}
}
const toggleEffects = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isEffectsOpen ? null : 'effects'
}
return {
toggleParticipants,
toggleChat,
toggleEffects,
isChatOpen: state?.showChat,
unreadMessages: state?.unreadMessages,
isParticipantsOpen,
isEffectsOpen,
}
}
@@ -4,6 +4,10 @@ import * as React from 'react'
import { supportsScreenSharing } from '@livekit/components-core'
import {
DisconnectButton,
LeaveIcon,
MediaDeviceMenu,
TrackToggle,
useMaybeLayoutContext,
usePersistentUserChoices,
} from '@livekit/components-react'
@@ -11,13 +15,10 @@ import {
import { mergeProps } from '@/utils/mergeProps.ts'
import { StartMediaButton } from '../components/controls/StartMediaButton'
import { useMediaQuery } from '../hooks/useMediaQuery'
import { useTranslation } from 'react-i18next'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '../components/controls/ChatToggle'
import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
/** @public */
export type ControlBarControls = {
@@ -36,7 +37,7 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
controls?: ControlBarControls
/**
* If `true`, the user's device choices will be persisted.
* This will enable the user to have the same device choices when they rejoin the room.
* This will enables the user to have the same device choices when they rejoin the room.
* @defaultValue true
* @alpha
*/
@@ -65,6 +66,7 @@ export function ControlBar({
onDeviceError,
...props
}: ControlBarProps) {
const { t } = useTranslation('rooms')
const [isChatOpen, setIsChatOpen] = React.useState(false)
const layoutContext = useMaybeLayoutContext()
React.useEffect(() => {
@@ -80,8 +82,26 @@ export function ControlBar({
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
variation ??= defaultVariation
const showIcon = React.useMemo(
() => variation === 'minimal' || variation === 'verbose',
[variation]
)
const showText = React.useMemo(
() => variation === 'textOnly' || variation === 'verbose',
[variation]
)
const browserSupportsScreenSharing = supportsScreenSharing()
const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false)
const onScreenShareChange = React.useCallback(
(enabled: boolean) => {
setIsScreenShareEnabled(enabled)
},
[setIsScreenShareEnabled]
)
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
const {
@@ -105,38 +125,71 @@ export function ControlBar({
return (
<div {...htmlProps}>
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Microphone}
showIcon={showIcon}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
>
{showText && t('controls.microphone')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="audioinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Camera}
showIcon={showIcon}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
>
{showText && t('controls.camera')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="videoinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
{browserSupportsScreenSharing && (
<ScreenShareToggle
<TrackToggle
source={Track.Source.ScreenShare}
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
showIcon={showIcon}
onChange={onScreenShareChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
/>
>
{showText &&
t(
isScreenShareEnabled
? 'controls.stopScreenShare'
: 'controls.shareScreen'
)}
</TrackToggle>
)}
<HandToggle />
<ChatToggle />
<ParticipantsToggle />
<OptionsButton />
<LeaveButton />
<DisconnectButton>
{showIcon && <LeaveIcon />}
{showText && t('controls.leave')}
</DisconnectButton>
<StartMediaButton />
</div>
)
@@ -30,12 +30,11 @@ import {
import { ControlBar } from './ControlBar'
import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css'
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { participantsStore } from '@/stores/participants'
import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
const LayoutWrapper = styled(
'div',
@@ -172,8 +171,8 @@ export function VideoConference({
])
/* eslint-enable react-hooks/exhaustive-deps */
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
return (
<div className="lk-video-conference" {...props}>
@@ -185,45 +184,32 @@ export function VideoConference({
>
<div className="lk-video-conference-inner">
<LayoutWrapper>
<div
style={{ display: 'flex', position: 'relative', width: '100%' }}
>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div className="lk-focus-layout-wrapper">
<FocusLayoutContainer>
<CarouselLayout tracks={carouselTracks}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div
className="lk-focus-layout-wrapper"
style={{ height: 'auto' }}
>
<FocusLayoutContainer>
<CarouselLayout
tracks={carouselTracks}
style={{
minWidth: '200px',
}}
>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
<MainNotificationToast />
</div>
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
<Chat
style={{ display: widgetState.showChat ? 'grid' : 'none' }}
messageFormatter={chatMessageFormatter}
messageEncoder={chatMessageEncoder}
messageDecoder={chatMessageDecoder}
/>
{sidePanel && <SidePanel />}
{showParticipants && <ParticipantsList />}
</LayoutWrapper>
<ControlBar />
</div>
@@ -1,8 +1,7 @@
import { Trans, useTranslation } from 'react-i18next'
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
import { logoutUrl, useUser } from '@/features/auth'
import { ProConnectButton } from '@/components/ProConnectButton'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
@@ -29,7 +28,9 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<ProConnectButton />
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
</>
)}
<H lvl={2}>{t('language.heading')}</H>
@@ -12,8 +12,6 @@ import {
import { AccountTab } from './tabs/AccountTab'
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useRef } from 'react'
const tabsStyle = css({
maxHeight: '40.625rem', // fixme size copied from meet settings modal
@@ -30,13 +28,13 @@ const tabListContainerStyle = css({
borderRight: '1px solid lightGray', // fixme poor color management
paddingY: '1rem',
paddingRight: '1.5rem',
flex: '0 0 16rem', // fixme size copied from meet settings modal
})
const tabPanelContainerStyle = css({
display: 'flex',
flexGrow: '1',
marginTop: '3.5rem',
minWidth: 0,
})
export type SettingsDialogExtended = Pick<
@@ -47,39 +45,25 @@ export type SettingsDialogExtended = Pick<
export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
// display only icon on small screen
const { t } = useTranslation('settings')
const dialogEl = useRef<HTMLDivElement>(null)
const { width } = useSize(dialogEl)
const isWideScreen = !width || width >= 800 // fixme - hardcoded 50rem in pixel
return (
<Dialog innerRef={dialogEl} {...props} role="dialog" type="flex">
<Dialog {...props} role="dialog" type="flex">
<Tabs orientation="vertical" className={tabsStyle}>
<div
className={tabListContainerStyle}
style={{
flex: isWideScreen ? '0 0 16rem' : undefined,
paddingTop: !isWideScreen ? '64px' : undefined,
paddingRight: !isWideScreen ? '1rem' : undefined,
}}
>
{isWideScreen && (
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
)}
<div className={tabListContainerStyle}>
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
<TabList border={false} aria-label="Chat log orientation example">
<Tab icon highlight id="1">
<RiAccountCircleLine />
{isWideScreen && t('tabs.account')}
{t('tabs.account')}
</Tab>
<Tab icon highlight id="2">
<RiSpeakerLine />
{isWideScreen && t('tabs.audio')}
{t('tabs.audio')}
</Tab>
<Tab icon highlight id="3">
<RiSettings3Line />
{isWideScreen && t('tabs.general')}
{t('tabs.general')}
</Tab>
</TabList>
</div>
@@ -4,12 +4,11 @@ import {
usePersistentUserChoices,
useRoomContext,
} from '@livekit/components-react'
import { logoutUrl, useUser } from '@/features/auth'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { css } from '@/styled-system/css'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { HStack } from '@/styled-system/jsx'
import { useState } from 'react'
import { ProConnectButton } from '@/components/ProConnectButton'
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -59,7 +58,9 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<ProConnectButton />
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
</>
)}
<HStack
@@ -1,55 +1,10 @@
import { DialogProps, Field, H } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import {
useIsSpeaking,
useMediaDeviceSelect,
useRoomContext,
} from '@livekit/components-react'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { isSafari } from '@/utils/livekit'
import { useTranslation } from 'react-i18next'
import { SoundTester } from '@/components/SoundTester'
import { HStack } from '@/styled-system/jsx'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import { ReactNode } from 'react'
type RowWrapperProps = {
heading: string
children: ReactNode[]
}
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
return (
<>
<H lvl={2}>{heading}</H>
<HStack
gap={0}
style={{
flexWrap: 'wrap',
}}
>
<div
style={{
flex: '1 1 215px',
minWidth: 0,
}}
>
{children[0]}
</div>
<div
style={{
width: '10rem',
justifyContent: 'center',
display: 'flex',
paddingLeft: '1.5rem',
}}
>
{children[1]}
</div>
</HStack>
</>
)
}
import { SoundTester } from '@/components/SoundTester.tsx'
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -58,9 +13,6 @@ type DeviceItems = Array<{ value: string; label: string }>
export const AudioTab = ({ id }: AudioTabProps) => {
const { t } = useTranslation('settings')
const { localParticipant } = useRoomContext()
const isSpeaking = useIsSpeaking(localParticipant)
const {
devices: devicesOut,
@@ -108,32 +60,20 @@ export const AudioTab = ({ id }: AudioTabProps) => {
return (
<TabPanel padding={'md'} flex id={id}>
<RowWrapper heading={t('audio.microphone.heading')}>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={
activeDeviceIdIn || getDefaultSelectedKey(itemsIn)
}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
style={{
width: '100%',
}}
/>
<>
{localParticipant.isMicrophoneEnabled ? (
<ActiveSpeaker isSpeaking={isSpeaking} />
) : (
<span>Micro désactivé</span>
)}
</>
</RowWrapper>
<H lvl={2}>{t('audio.microphone.heading')}</H>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={activeDeviceIdIn || getDefaultSelectedKey(itemsIn)}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
/>
{/* Safari has a known limitation where its implementation of 'enumerateDevices' does not include audio output devices.
To prevent errors or an empty selection list, we only render the speakers selection field on non-Safari browsers. */}
{!isSafari() && (
<RowWrapper heading={t('audio.speakers.heading')}>
<>
<H lvl={2}>{t('audio.speakers.heading')}</H>
<Field
type="select"
label={t('audio.speakers.label')}
@@ -145,12 +85,9 @@ export const AudioTab = ({ id }: AudioTabProps) => {
setActiveMediaDeviceOut(key as string)
}
{...disabledProps}
style={{
minWidth: 0,
}}
/>
<SoundTester />
</RowWrapper>
</>
)}
</TabPanel>
)
@@ -1,31 +0,0 @@
import { useEffect } from 'react'
import { Crisp } from 'crisp-sdk-web'
import { ApiUser } from '@/features/auth/api/ApiUser'
export const initializeSupportSession = (user: ApiUser) => {
if (!Crisp.isCrispInjected()) return
const { id, email } = user
Crisp.setTokenId(`meet-${id}`)
Crisp.user.setEmail(email)
}
export const terminateSupportSession = () => {
if (!Crisp.isCrispInjected()) return
Crisp.setTokenId()
Crisp.session.reset()
}
export type useSupportProps = {
id?: string
}
// Configure Crisp chat for real-time support across all pages.
export const useSupport = ({ id }: useSupportProps) => {
useEffect(() => {
if (!id || Crisp.isCrispInjected()) return
Crisp.configure(id)
Crisp.setHideOnMobile(true)
}, [id])
return null
}
+3 -8
View File
@@ -2,16 +2,13 @@ import { Link } from 'wouter'
import { css } from '@/styled-system/css'
import { Stack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { Text, Button } from '@/primitives'
import { A, Text, Button } from '@/primitives'
import { SettingsButton } from '@/features/settings'
import { logoutUrl, useUser } from '@/features/auth'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
import { Feedback } from '@/components/Feedback'
import { Menu } from '@/primitives/Menu'
import { MenuList } from '@/primitives/MenuList'
import { ProConnectButton } from '@/components/ProConnectButton'
import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { terminateSupportSession } from '@/features/support/hooks/useSupport'
export const Header = () => {
const { t } = useTranslation()
@@ -66,7 +63,7 @@ export const Header = () => {
<nav>
<Stack gap={1} direction="row" align="center">
{isLoggedIn === false && !isHome && (
<ProConnectButton hint={false} />
<A href={authUrl()}>{t('login')}</A>
)}
{!!user && (
<Menu>
@@ -82,8 +79,6 @@ export const Header = () => {
items={[{ value: 'logout', label: t('logout') }]}
onAction={(value) => {
if (value === 'logout') {
terminateAnalyticsSession()
terminateSupportSession()
window.location.href = logoutUrl()
}
}}
+2 -6
View File
@@ -1,5 +1,5 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "",
"cancel": "",
"closeDialog": "",
@@ -12,11 +12,7 @@
},
"loading": "",
"loggedInUserTooltip": "",
"login": {
"buttonLabel": "",
"linkLabel": "",
"link": ""
},
"login": "Anmelden",
"logout": "",
"notFound": {
"heading": ""
+2 -2
View File
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "",
"description": "",
"copy": "",
"copied": "",
"permissions": ""
"copy": "",
"inputLabel": ""
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "",
"joined": {
"description": ""
},
"raised": {
"description": "",
"cta": ""
}
}
+9 -72
View File
@@ -4,35 +4,18 @@
"heading": ""
},
"join": {
"videoinput": {
"choose": "",
"disable": "",
"enable": "",
"label": "",
"placeholder": ""
},
"audioinput": {
"choose": "",
"disable": "",
"enable": "",
"label": ""
},
"camlabel": "",
"heading": "",
"joinLabel": "",
"joinMeeting": "",
"toggleOff": "",
"toggleOn": "",
"userLabel": "",
"usernameHint": "",
"usernameLabel": ""
"micLabel": "",
"userLabel": ""
},
"leaveRoomPrompt": "",
"shareDialog": {
"copy": "",
"copied": "",
"copy": "",
"heading": "",
"description": "",
"permissions": ""
"inputLabel": ""
},
"error": {
"createRoom": {
@@ -49,14 +32,6 @@
"open": "",
"closed": ""
},
"hand": {
"raise": "",
"lower": ""
},
"screenShare": {
"start": "",
"stop": ""
},
"leave": "",
"participants": {
"open": "",
@@ -69,50 +44,12 @@
"feedbacks": "",
"support": "",
"settings": "",
"username": "",
"effects": ""
"username": ""
}
},
"effects": {
"activateCamera": "",
"notAvailable": "",
"heading": "",
"blur": {
"light": "",
"normal": "",
"apply": "",
"clear": ""
},
"experimental": ""
},
"sidePanel": {
"heading": {
"participants": "",
"effects": ""
},
"content": {
"participants": "",
"effects": ""
},
"closeButton": ""
},
"participants": {
"subheading": "",
"contributors": "",
"collapsable": {
"open": "",
"close": ""
},
"you": "",
"muteYourself": "",
"muteParticipant": "",
"muteParticipantAlert": {
"description": "",
"confirm": "",
"cancel": ""
},
"raisedHands": "",
"lowerParticipantHand": "",
"lowerParticipantsHand": ""
"heading": "",
"closeButton": "",
"you": ""
}
}
+3 -7
View File
@@ -1,10 +1,10 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "Back to homescreen",
"cancel": "Cancel",
"closeDialog": "Close dialog",
"error": {
"heading": "An error occurred while loading the page"
"heading": "An error occured while loading the page"
},
"feedbackAlert": "Give us feedback",
"forbidden": {
@@ -12,11 +12,7 @@
},
"loading": "Loading…",
"loggedInUserTooltip": "Logged in as…",
"login": {
"buttonLabel": "Login with ProConnect",
"linkLabel": "What is ProConnect? - new tab",
"link": "What is ProConnect?"
},
"login": "Login",
"logout": "Logout",
"notFound": {
"heading": "Page not found"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Create a meeting",
"heading": "Welcome in Visio",
"heading": "Welcome in Meet",
"intro": "Work easily, from anywhere.",
"joinInputError": "Use a meeting link or code. Examples:",
"joinInputExample": "URL or 10-letter code",
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Your connection details",
"description": "Send this link to the people you want to invite to the meeting. They will be able to join without Agent Connect.",
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
"copied": "Copied",
"copy": "Copy",
"inputLabel": "Meeting link"
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "A contributor",
"joined": {
"description": "{{name}} has joined the room"
},
"raised": {
"description": "{{name}} has raised their hand.",
"cta": "Open waiting list"
}
}
+15 -76
View File
@@ -1,38 +1,21 @@
{
"feedback": {
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
"heading": "Help us improve Visio"
"heading": "Help us improve Meet"
},
"join": {
"videoinput": {
"choose": "Select camera",
"disable": "Disable camera",
"enable": "Enable camera",
"label": "Camera",
"placeholder": "Enable camera to see the preview"
},
"audioinput": {
"choose": "Select microphone",
"disable": "Disable microphone",
"enable": "Enable microphone",
"label": "Microphone"
},
"heading": "Verify your settings",
"camlabel": "Camera",
"heading": "Join the meeting",
"joinLabel": "Join",
"joinMeeting": "Join meeting",
"toggleOff": "Click to turn off",
"toggleOn": "Click to turn on",
"userLabel": "",
"usernameHint": "Shown to other participants",
"usernameLabel": "Your name"
"micLabel": "Microphone",
"userLabel": "Your name"
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
"copy": "Copy the meeting 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."
"copied": "Copied",
"copy": "Copy",
"heading": "Share the meeting link",
"inputLabel": "Meeting link"
},
"error": {
"createRoom": {
@@ -43,18 +26,12 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Share screen",
"stopScreenShare": "Stop screen share",
"chat": {
"open": "Close the chat",
"closed": "Open the chat"
},
"hand": {
"raise": "Raise hand",
"lower": "Lower hand"
},
"screenShare": {
"start": "Share screen",
"stop": "Stop screen share"
},
"leave": "Leave",
"participants": {
"open": "Hide everyone",
@@ -67,50 +44,12 @@
"feedbacks": "Give us feedbacks",
"support": "Get Help on Tchap",
"settings": "Settings",
"username": "Update Your Name",
"effects": "Apply effects"
"username": "Update Your Name"
}
},
"effects": {
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
"notAvailable": "The blur effect will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome :(",
"heading": "Blur",
"blur": {
"light": "Light blur",
"normal": "Blur",
"apply": "Enable blur",
"clear": "Disable blur"
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effects"
},
"content": {
"participants": "participants",
"effects": "effects"
},
"closeButton": "Hide {{content}}"
},
"participants": {
"subheading": "In room",
"you": "You",
"contributors": "Contributors",
"collapsable": {
"open": "Open {{name}} list",
"close": "Close {{name}} list"
},
"muteYourself": "Close your mic",
"muteParticipant": "Close the mic of {{name}}",
"muteParticipantAlert": {
"description": "Mute {{name}} for all participants? {{name}} will be the only one who can unmute themselves.",
"confirm": "Mute",
"cancel": "Cancel"
},
"raisedHands": "Raised hands",
"lowerParticipantHand": "Lower {{name}}'s hand",
"lowerParticipantsHand": "Lower all hands"
"heading": "Participants",
"closeButton": "Hide participants",
"you": "You"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
"heading": "Account",
"youAreNotLoggedIn": "You are not logged in.",
"nameLabel": "Your Name",
"nameLabel": "Votre Nom",
"authentication": "Authentication"
},
"audio": {
+2 -6
View File
@@ -1,5 +1,5 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "Retour à l'accueil",
"cancel": "Annuler",
"closeDialog": "Fermer la fenêtre modale",
@@ -12,11 +12,7 @@
},
"loading": "Chargement…",
"loggedInUserTooltip": "Connecté en tant que…",
"login": {
"buttonLabel": "S'identifier avec ProConnect",
"linkLabel": "Qu'est-ce que ProConnect ? - nouvelle fenêtre",
"link": "Qu'est-ce que ProConnect ?"
},
"login": "Se connecter",
"logout": "Se déconnecter",
"notFound": {
"heading": "Page introuvable"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Créer une réunion",
"heading": "Visio",
"heading": "Meet",
"intro": "Collaborez en toute simplicité, où que vous soyez.",
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Vos informations de connexion",
"description": "Envoyez ce lien aux personnes que vous souhaitez inviter à la réunion. Ils pourront la rejoindre sans Agent Connect.",
"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."
"copied": "Lien copié",
"copy": "Copier le lien",
"inputLabel": "Lien vers la réunion"
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "Un contributeur",
"joined": {
"description": "{{name}} participe à l'appel"
},
"raised": {
"description": "{{name}} a levé la main.",
"cta": "Ouvrir la file d'attente"
}
}
+15 -76
View File
@@ -1,38 +1,21 @@
{
"feedback": {
"body": "Remplissez le formulaire disponible dans l'entête du site pour nous donner votre avis sur l'outil. Vos retours sont précieux ! Merci.",
"heading": "Aidez-nous à améliorer Visio"
"heading": "Aidez-nous à améliorer Meet"
},
"join": {
"videoinput": {
"choose": "Choisir la webcam",
"disable": "Désactiver la webcam",
"enable": "Activer la webcam",
"label": "Webcam",
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
},
"audioinput": {
"choose": "Choisir le micro",
"disable": "Désactiver le micro",
"enable": "Activer le micro",
"label": "Microphone"
},
"heading": "Vérifiez vos paramètres",
"camlabel": "Webcam",
"heading": "Rejoindre la réunion",
"joinLabel": "Rejoindre",
"joinMeeting": "Rejoindre la réjoindre",
"toggleOff": "Cliquez pour désactiver",
"toggleOn": "Cliquez pour activer",
"userLabel": "",
"usernameHint": "Affiché aux autres participants",
"usernameLabel": "Votre nom"
"micLabel": "Micro",
"userLabel": "Votre nom"
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"heading": "Votre réunion est prête",
"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."
"copied": "Lien copié",
"copy": "Copier le lien",
"heading": "Partager le lien vers la réunion",
"inputLabel": "Lien vers la réunion"
},
"error": {
"createRoom": {
@@ -43,18 +26,12 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Partager l'écran",
"stopScreenShare": "Arrêter le partage",
"chat": {
"open": "Masquer le chat",
"closed": "Afficher le chat"
},
"hand": {
"raise": "Lever la main",
"lower": "Baisser la main"
},
"screenShare": {
"start": "Partager l'écran",
"stop": "Arrêter le partage"
},
"leave": "Quitter",
"participants": {
"open": "Masquer les participants",
@@ -67,50 +44,12 @@
"feedbacks": "Partager votre avis",
"support": "Obtenir de l'aide sur Tchap",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Appliquer des effets"
"username": "Choisir votre nom"
}
},
"effects": {
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
"notAvailable": "L'effet de flou sera bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome :(",
"heading": "Flou",
"blur": {
"light": "Léger flou",
"normal": "Flou",
"apply": "Appliquer le flou",
"clear": "Désactiver le flou"
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet sur tous les navigateurs et une meilleur qualité."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effets"
},
"content": {
"participants": "les participants",
"effects": "les effets"
},
"closeButton": "Masquer {{content}}"
},
"participants": {
"subheading": "Dans la réunion",
"you": "Vous",
"contributors": "Contributeurs",
"collapsable": {
"open": "Ouvrir la liste {{name}}",
"close": "Fermer la liste {{name}}"
},
"muteYourself": "Couper votre micro",
"muteParticipant": "Couper le micro de {{name}}",
"muteParticipantAlert": {
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
"confirm": "Couper le micro",
"cancel": "Annuler"
},
"raisedHands": "Mains levées",
"lowerParticipantHand": "Baisser la main de {{name}}",
"lowerParticipantsHand": "Baisser la main de tous les participants"
"heading": "Participants",
"closeButton": "Masquer les participants",
"you": "Vous"
}
}
+4 -5
View File
@@ -134,11 +134,10 @@ export const Checkbox = ({
>
<StyledCheckbox {...props}>
{(renderProps) => {
if (renderProps.isInvalid && !!props.validate) {
setError(props.validate(renderProps.isSelected))
} else {
setError(null)
}
renderProps.isInvalid && !!props.validate
? setError(props.validate(renderProps.isSelected))
: setError(null)
return (
<>
<div className="mt-Checkbox-checkbox">
+1 -4
View File
@@ -10,7 +10,6 @@ import {
} from 'react-aria-components'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
import { text } from './Text'
import { MutableRefObject } from 'react'
const StyledModalOverlay = styled(ModalOverlay, {
base: {
@@ -62,7 +61,6 @@ export type DialogProps = RACDialogProps & {
*/
onOpenChange?: (isOpen: boolean) => void
type?: 'flex'
innerRef?: MutableRefObject<HTMLDivElement | null>
}
export const Dialog = ({
@@ -71,7 +69,6 @@ export const Dialog = ({
onClose,
isOpen,
onOpenChange,
innerRef,
...dialogProps
}: DialogProps) => {
const isAlert = dialogProps['role'] === 'alertdialog'
@@ -96,7 +93,7 @@ export const Dialog = ({
<VerticallyOffCenter>
<Div margin="auto" width="fit-content" maxWidth="full">
<Div margin="1rem" pointerEvents="auto">
<Box size="sm" type={boxType} ref={innerRef}>
<Box size="sm" type={boxType}>
{!!title && (
<Heading
slot="title"
-1
View File
@@ -23,7 +23,6 @@ import { Div } from './Div'
const FieldWrapper = styled('div', {
base: {
marginBottom: 'textfield',
minWidth: 0,
},
})
+1 -1
View File
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
const label = typeof item === 'string' ? item : item.label
return (
<MenuItem
className={menuItemRecipe({ extraPadding: true })}
className={menuItemRecipe()}
key={value}
id={value as string}
onAction={() => {
-3
View File
@@ -43,9 +43,6 @@ const StyledButton = styled(Button, {
const StyledSelectValue = styled(SelectValue, {
base: {
textOverflow: 'ellipsis',
overflow: 'hidden',
textWrap: 'nowrap',
'&[data-placeholder]': {
color: 'default.subtle-text',
fontStyle: 'italic',
-10
View File
@@ -1,10 +0,0 @@
import { styled } from '@/styled-system/jsx'
import { Separator as RACSeparator } from 'react-aria-components'
export const Separator = styled(RACSeparator, {
base: {
height: '1px',
background: 'gray.400',
margin: '4px 0',
},
})
@@ -25,7 +25,7 @@ export const TooltipWrapper = ({
children: ReactNode
} & TooltipWrapperProps) => {
return tooltip ? (
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
+3 -16
View File
@@ -23,9 +23,6 @@ export const buttonRecipe = cva({
'&[data-selected]': {
backgroundColor: 'colorPalette.active',
},
'&[data-disabled]': {
cursor: 'auto',
},
},
variants: {
size: {
@@ -73,13 +70,6 @@ export const buttonRecipe = cva({
backgroundColor: 'success.subtle!',
},
},
text: {
color: 'primary',
'&[data-hovered]': {
background: 'gray.100 !important',
color: 'primary !important',
},
},
danger: {
colorPalette: 'danger',
borderColor: 'danger.600',
@@ -150,7 +140,6 @@ export const buttonRecipe = cva({
legacyStyle: {
true: {
borderColor: 'gray.400',
transition: 'border 200ms, background 200ms, color 200ms',
'&[data-hovered]': {
borderColor: 'gray.500',
},
@@ -158,12 +147,10 @@ export const buttonRecipe = cva({
borderColor: 'gray.500',
},
'&[data-selected]': {
backgroundColor: '#1d4ed8',
color: 'white',
borderColor: 'gray.500',
background: '#e5e7eb',
borderColor: 'gray.400',
'&[data-hovered]': {
borderColor: '#6b7280',
backgroundColor: '#1e40af',
backgroundColor: 'gray.300',
},
},
},
-2
View File
@@ -2,10 +2,8 @@ import { proxy } from 'valtio'
type State = {
showHeader: boolean
sidePanel: 'participants' | 'effects' | null
}
export const layoutStore = proxy<State>({
showHeader: false,
sidePanel: null,
})
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
showParticipants: boolean
}
export const participantsStore = proxy<State>({
showParticipants: false,
})
+1 -11
View File
@@ -1,10 +1,4 @@
import {
getBrowser,
LocalParticipant,
LogLevel,
Participant,
setLogLevel,
} from 'livekit-client'
import { getBrowser, LogLevel, setLogLevel } from 'livekit-client'
export const silenceLiveKitLogs = (shouldSilenceLogs: boolean) => {
setLogLevel(shouldSilenceLogs ? LogLevel.silent : LogLevel.debug)
@@ -21,7 +15,3 @@ export function isChromiumBased(): boolean {
export function isSafari(): boolean {
return getBrowser()?.name === 'Safari'
}
export function isLocal(p: Participant) {
return p instanceof LocalParticipant
}
+35 -44
View File
@@ -18,74 +18,65 @@ sops:
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCQnRJY0xWNFVRYkR0QTND
Rkc5dDRITUpRMW1EVlhEUk9xN0ttbElrdEJvClBMVnVKMXhGMThTcTVaN0s3OG1v
S0U2UUNYZ3FzcXUwVXRyS1hPZkpxcmsKLS0tIHhWei9KVmxFaXgzRUxmMWU2RmhG
Y1ZQekpQS3poSWxHZXUvbitlc2lIczAKjLZlgUSz51W4GHirUn352eFPSxIK1/Wf
N+kzoUvMLmwwfHDztFFYLOEE9x1zx7GoPGG7fN9bTG7GWgcRdd7NUw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZG8rTjAzcVNtVHdLZmJI
Mm4vemNrV1dDUExEZ1R1UGJmWjN3aWw1VXc0CjF1MVhhUTV6ckl1OEFNSjBJUCta
MTE3QU04RDJKMWlWcHhDSG1NTmZyTkEKLS0tIHh5UHRqckUxZWZLUDl4d3FDdHJs
Y0VMc0llMytmMWNUOW56d3ltTHpwZ2cKEHkn6wuHNeMTk+E6nEMpEJZ6wpdXSi3k
FkzXRa6SudAgA4R6K9EieHKPdiNvi0IsCJOhLpiNQtENu9poF5ozqQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3aTVDMDNUUFFVQlE3Ykpl
ZlFQUHQ0UUdlTU54WlEvanBWeGVlYnd1aEZFClQ3Y1ZSOHp6QUt1UXBLV3NWaGJ4
T2ZwKzZXcXh1Z2llQk9SN1RRLzNOdGcKLS0tIHN0YTFpL05SQUc1WTRsQ2Z0ZGtu
ZjhoUHhFbkMvRUhscy9LY05xbXFZVlEKXrt6UPLOprcnsTFX1WCF/pIWXmjiPGEx
7NNbnAl+A9yfheCeiJdWcj5ZBCa6Nx2udVMNjQ7ITMzhIY4BBSicuQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAva3lFTTM2ZUJpcnozNmJt
ZHAvRndkSjRCVEZYN29wMWFZVmV5ekNiYW5FCkI3RWUvVTdVZ2dJeWVSY085SW5G
UWNOempRQlFCUUlmL0RIR1ZZZVI5cDQKLS0tIEpwZkVodENLM1VnQ1o1TmhRSFFi
ZEF5MUNVdHRKcW9aK0M2M0ZVNDBxbjgKw8Wp06PcoStrO6ppsOR5zWjXbYrP64Dv
XAEHQMa7vyvuu12YIa/fpyDM4HrsrPq3OWudxuWS6p0X8xmPYrXm6g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1Y3JyU3JPOWdiamd0cEFV
UWRUdUQ5V3ZnQXQ0YURLZ3RiY0toN29vNkhJCnIzYWdkV3puVWQ5Nm5SVGpobllB
OW5WWkVLaE55eUs2OWNDWkZiOGNuZW8KLS0tIHN3eGVST1BRMU1JRWNRZGNJeGdi
SWJXQ2VMZ3kyWElBSjBKc0tQWmIwMjgKLDakU3iHwVTQBYGR0d2TFFdLdsct49y1
/S8vydlcx08L0yWHbfamhiYJE3BZbRXZue6z5irBPKEVjGD42aSREw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1OU9mZHZFanpTQjE3aGly
VVZDWFh5ZmRHV1YwWUtsZVd6R090SnkxVVNVCmM1UWwwaFlqTHlhSGFRblZBbVhl
aEhtWWp1N1lVTDNqOFJBL0swVFl3Y3cKLS0tIG5ZTHAveFdLdGhQeUc2Tzh1d0dw
UzRhNGNTZFR3U3BLNFhCdFp6aU5uZVEKL4K5jFjPfMp/fMA8+nQerj6PE5zvGeHW
1SuHwnDiglKmksj8Gy+7spwLQCmo11JCnW2gXKktVIe5XOyhortq3Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKUklKTzRzdHZiZjdHSmY2
WCs3ZFVodFBGcDdvZWZaaHNnbG9mdFk2WVJjCjUwZjA2Qi9CR1JYdjJ3VFlhZDZR
REEyZStrTjIwbWdCdVU5NWo2Y2hBemsKLS0tIFZmWVh0Z1BFUGRjZExMNlNJSFVJ
VnlWY3U4dW4ycXJCOVVUUlJxYzVONGsKy47vHe+awhJyI/pSUOhvUiOt+jtn+Vwg
Rlm93bJr0GNVWYm521r/I409s3TMitQerweGnl3u7t1FaKX7oI1qBQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Z1lXM0tDRzJnMytDZHBV
ZlpCcTg0U3RGeEVzN2dBMkFYa0FBMFVKMWw4Cm5KeWMvdFBRNHFJWFNSS3NVWlNQ
dnhkc2F2MFVTMmVHT2U2STdUWWcvNzAKLS0tIHkzQXltQW5TWHVrektQU1hES1ll
UnVld1laTWRmTlkzWHRKRjJmWVJhbU0KANxoSnhDbxsja+Eo8MVCGv6iThNmlo/m
y0RXVNVqNlqTreT0F/SKmP74W3lF30nwsfrOywMQyu75k8p5MGwUuQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVUE9nVk1SMUp5WW9zYnZs
OFBYWmpwZW56S0J0Qk43dmlCWG5RV0RIKzM0CkVNbzI5VSs3VVdaT2xnVEhGdW9h
QW02K0ZmeGxsSDZMTGdETjkwd2xVd2MKLS0tIHp2WWQyUFphMmlZaDBZR0JQZ095
UTVaYXl6SkZUNElUdUlZcU8rditFd3cK0CqMiDzEItfB/T0K8YEncp9HuKWVTy7q
2LgHBQJi35sdBviEpsHZt7BlHTKanbmB5S9oUexNq+3wUP9e1n5CGA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwUm9oT3RYbGZXaW5CM20r
QlYyeGZjMG05MFNzdndCU2dUemZqdXF5Um1FCkZWdVlubW1MZ3NwM3V2S3dQRFA1
cHVvOXBMMGtMdkF4T2s3Wk9wYkJUZUEKLS0tIFBRM3FWYTRrVDNwZStwQWlpbEYx
dXA5WE5udnhhMVdLTk1jMzJuU3VWaTQKaxTpyqi5fjmOFR4qOxm+wSqWDxb/96QI
rHGUI+CqMVKZ6w1fMH0uanvCuw7Q9rbmsKB+DsjARMLFGqxzP/psUA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDdRaEVCemd5T0ozSmtp
NllJa0tURVhxTXlZQS8vNUZxbGFQVnhRMDJjClhlaE53WTRpeEtVSGErYUdyZk1h
SloxZGNUdFRDZ1J4bzdneFUyNXdXdkUKLS0tIGdxU3RCUEVmcTdnZnFidkROdXJS
dVdQUUdVUUhpVVh6SytRV1V0RHV6cDQKagJA6w43n8yh119PrJltPPh/EbHIKjfH
G8RfgXBrf5iWdWsyD8haDL6WsjdbVQsbPCz+ucULfnZ1Dn8A/3yQUQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMmVOaHV1Unl4UmUrVXEy
REJIYWZhQmt3R0R5RFRveTl4Z0JaZlpKNmd3Ckl0dkFUaG90TE5odVh0YVprMVZj
T2tXS0dSRGVVczdhb1dlVlV0L3JZUncKLS0tIGo2RDBPZXNzU2dQQlJhY0NBS2ps
RW92OFVTS3d2L3UxOXNrVUFSVFI5TXMKn3pdHbXxBccG6Q+gWPVQK/5wiIKkzdhm
HiNzezStrkFHf/lsFS2LNgYkfMMzBQ4rJj7oQkrD1Z8j6qRld3zzLg==
-----END AGE ENCRYPTED FILE-----
- recipient: age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIbGgxRG5sZHFPZm5OTnZw
L21JUnFDOTNZUitYNHJad3FnYkk2aDlQRzM0CnI1NXZDcEtQdjBWaVhJMElIOEZU
KzFjT0p4OVpzUFlnR1Q5cm5SVnhGaEUKLS0tIFliVVQ4VFg4QVJxazdyZG9PbE9M
M3NzZFRKMFkxZHZ1Rmh0VWFWMDZTQlEKxKVEvnnV56t/RSvP94TgjW1Do68Tn2N2
tJMI8VIp7rs5vxaToois6CMuhVONsl1CBozAEV8pqS5O830RGa1eog==
-----END AGE ENCRYPTED FILE-----
- recipient: age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBta3VwOGQrSHdENGZLZUpq
SGw0Z2YwYm8vZ0h4Y01UclIrQlRVNWVJaHlRCmFMQkp1aGc5R1JrMVdPNmwvbkYv
NkxOc3pYY2E5dVVFejZJaitSb0hRUVUKLS0tIGFzclh3VXhBc3NYQjZlUmNUSk10
S2xqMEpBcEpaOHdBNUtvOWxsVUhLLzQKGT3grtnocBkaCKXbtH7cZu/ZP9MKsAjt
ZNmS1GahpBx2lVEbfJNLfwId+IJ6kOIyHj42g9yIQFfKTPKE32Ht8A==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzeWN4ZE5Ya3RPV2VKRFdv
UGQ4QTJUZFovSXpQREhNWXdQbktVUHFQaUhjCmM1dXNxbGdlVjBWNjc5L2E5QlBy
NjI0S251cjZvYU1nQ2swWXVDdjdhSG8KLS0tIHdFNXptN1VBNytFNW40MUsxTGFM
WlVnc3VLY1ZReDFxUVlqWU9rM0xMQ00Kq/ckdP1N4BDBo0pSH9pp36skIacwNm3d
utiuCgG16Hqe6YDb3agx81BocuSJ9oMOdpegoztnkkBDDP0F+e/VuQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-07-18T13:08:42Z"
mac: ENC[AES256_GCM,data:a/uHyw9V/SMIePV9nPf+wJgPg+YDYLJGYy7NMLBrBgCXtBWHHonSNjzdmtjix1bW2y+cU0gMqodrtqR1cJGBmXr4NRY7NJqgLWE9rEdYfG7BnfqsWmvAaTIrSs7QMZWkEic7ys/bXoA5BZoau3olhVqIO2A/iyBtoMU9Hv7hPlo=,iv:gaqSCUbN7cxWPNrFPDTl7xNxpOZL6GY/swD/MDCiRqk=,tag:Oz0f/DyD3KGV/9Rprj/1Xw==,type:str]
@@ -49,8 +49,6 @@ backend:
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
ANALYTICS_KEY: xwhoIMCZ8PBRjQ2t
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
migrate:
@@ -97,11 +95,3 @@ ingress:
ingressAdmin:
enabled: true
host: meet.127.0.0.1.nip.io
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
@@ -0,0 +1,127 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.4"
backend:
migrateJobAnnotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-preprod.beta.numerique.gouv.fr,https://meet-preprod.beta.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet-preprod.beta.numerique.gouv.fr
DJANGO_SUPERUSER_EMAIL:
secretKeyRef:
name: backend
key: DJANGO_SUPERUSER_EMAIL
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
key: DJANGO_SECRET_KEY
DJANGO_SETTINGS_MODULE: meet.settings
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
DJANGO_SUPERUSER_PASSWORD:
secretKeyRef:
name: backend
key: DJANGO_SUPERUSER_PASSWORD
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
DJANGO_EMAIL_PORT: 465
DJANGO_EMAIL_USE_SSL: True
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
OIDC_RP_CLIENT_ID:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_ID
OIDC_RP_CLIENT_SECRET:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-preprod.beta.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet-preprod.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: host
DB_NAME:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: database
DB_USER:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: username
DB_PASSWORD:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: password
DB_PORT:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: port
POSTGRES_USER:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: username
POSTGRES_DB:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: database
POSTGRES_PASSWORD:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: password
REDIS_URL:
secretKeyRef:
name: redis.redis.libre.sh
key: url
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET:
secretKeyRef:
name: backend
key: LIVEKIT_API_SECRET
LIVEKIT_API_KEY:
secretKeyRef:
name: backend
key: LIVEKIT_API_KEY
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
ALLOW_UNREGISTERED_ROOMS: False
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.4"
ingress:
enabled: true
host: meet-preprod.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingressAdmin:
enabled: true
host: meet-preprod.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
@@ -1,16 +1,16 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.7"
tag: "v0.1.4"
backend:
migrateJobAnnotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: https://visio.numerique.gouv.fr,https://meet.numerique.gouv.fr
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: visio.numerique.gouv.fr,meet.numerique.gouv.fr
DJANGO_ALLOWED_HOSTS: meet.numerique.gouv.fr
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
@@ -43,11 +43,11 @@ backend:
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio.numerique.gouv.fr
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://visio.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://visio.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://visio.numerique.gouv.fr
LOGIN_REDIRECT_URL: https://meet.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
@@ -96,9 +96,6 @@ backend:
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
ANALYTICS_KEY: mwuxTKi8o2xzWH54
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio.numerique.gouv.fr'}"
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
createsuperuser:
command:
@@ -112,39 +109,20 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.7"
tag: "v0.1.4"
ingress:
enabled: true
host: visio.numerique.gouv.fr
host: meet.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
ingressAdmin:
enabled: true
host: visio.numerique.gouv.fr
host: meet.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/auth
posthog:
ingress:
enabled: true
host: product.visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/upstream-vhost: eu.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https
ingressAssets:
enabled: true
host: product.visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/upstream-vhost: eu-assets.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https

Some files were not shown because too many files have changed in this diff Show More