Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6544c895dc |
@@ -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"
|
||||
@@ -8,4 +8,3 @@ creation_rules:
|
||||
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa #marie
|
||||
- age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw #argocd
|
||||
- age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9 #manuuu
|
||||
- age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m # github-repo
|
||||
|
||||
@@ -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
@@ -13,9 +13,7 @@
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchPackageNames": [
|
||||
"eslint"
|
||||
]
|
||||
"matchPackageNames": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
Submodule secrets updated: 8ef9f4513a...f5fbc16e6e
@@ -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"),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+2768
-2462
File diff suppressed because it is too large
Load Diff
+27
-32
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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,5 +1,4 @@
|
||||
export const keys = {
|
||||
user: 'user',
|
||||
room: 'room',
|
||||
config: 'config',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 +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>
|
||||
)
|
||||
|
||||
@@ -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>'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>'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>
|
||||
)
|
||||
}
|
||||
+28
-34
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
-82
@@ -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>
|
||||
)
|
||||
}
|
||||
-27
@@ -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>
|
||||
)
|
||||
}
|
||||
-163
@@ -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>
|
||||
)
|
||||
}
|
||||
-109
@@ -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>
|
||||
)
|
||||
}
|
||||
+113
-60
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+16
-10
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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": ""
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"laterMeetingDialog": {
|
||||
"heading": "",
|
||||
"description": "",
|
||||
"copy": "",
|
||||
"copied": "",
|
||||
"permissions": ""
|
||||
"copy": "",
|
||||
"inputLabel": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"defaultName": "",
|
||||
"joined": {
|
||||
"description": ""
|
||||
},
|
||||
"raised": {
|
||||
"description": "",
|
||||
"cta": ""
|
||||
}
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,7 +23,6 @@ import { Div } from './Div'
|
||||
const FieldWrapper = styled('div', {
|
||||
base: {
|
||||
marginBottom: 'textfield',
|
||||
minWidth: 0,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,10 +2,8 @@ import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
showHeader: boolean
|
||||
sidePanel: 'participants' | 'effects' | null
|
||||
}
|
||||
|
||||
export const layoutStore = proxy<State>({
|
||||
showHeader: false,
|
||||
sidePanel: null,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
showParticipants: boolean
|
||||
}
|
||||
|
||||
export const participantsStore = proxy<State>({
|
||||
showParticipants: false,
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user