mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 20:29:09 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1103902c12 | |||
| 812016d09c | |||
| cadc5c0034 | |||
| b083d837f8 | |||
| db8445f4ab | |||
| 0e5c2be445 | |||
| 0131a65509 | |||
| fb3727e73e | |||
| 3391165e4b | |||
| 0be94aa572 | |||
| b309f91095 | |||
| c50a749293 | |||
| cd9e7c8a1f | |||
| 9d0767ccfe | |||
| 021e52d9eb | |||
| 607a5fc99d | |||
| 756be17cc7 | |||
| 00fa7beebd | |||
| b9d13de591 | |||
| 7896890ffc | |||
| a2043abb51 | |||
| 64bb1b3bb5 | |||
| a1a56402d1 | |||
| 582d3774a4 | |||
| fab08cfaf8 | |||
| 550c48f29e | |||
| 0c64580abf | |||
| 0ad9b7e233 | |||
| b9bcc45cce | |||
| bcd285e368 | |||
| c0ad98eb34 | |||
| 178278235e | |||
| 11664956c7 | |||
| f020979188 | |||
| da5c8d6773 | |||
| 31051cd6c4 | |||
| c51058e6ac | |||
| 3ba913bb21 | |||
| 95116f70e8 | |||
| 5b282b62e7 | |||
| 5e74fce6e2 | |||
| 96b279b350 | |||
| fc4ad562ae | |||
| 480a8df31a |
+3
-1
@@ -13,7 +13,9 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"groupName": "ignored js dependencies",
|
"groupName": "ignored js dependencies",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"matchPackageNames": []
|
"matchPackageNames": [
|
||||||
|
"eslint"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ class Base(Configuration):
|
|||||||
EMAIL_HOST_PASSWORD = values.Value(None)
|
EMAIL_HOST_PASSWORD = values.Value(None)
|
||||||
EMAIL_PORT = values.PositiveIntegerValue(None)
|
EMAIL_PORT = values.PositiveIntegerValue(None)
|
||||||
EMAIL_USE_TLS = values.BooleanValue(False)
|
EMAIL_USE_TLS = values.BooleanValue(False)
|
||||||
|
EMAIL_USE_SSL = values.BooleanValue(False)
|
||||||
EMAIL_FROM = values.Value("from@example.com")
|
EMAIL_FROM = values.Value("from@example.com")
|
||||||
|
|
||||||
AUTH_USER_MODEL = "core.User"
|
AUTH_USER_MODEL = "core.User"
|
||||||
|
|||||||
+10
-10
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.35.10",
|
"boto3==1.35.19",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.1.0",
|
||||||
"celery[redis]==5.4.0",
|
"celery[redis]==5.4.0",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
@@ -36,11 +36,11 @@ dependencies = [
|
|||||||
"django-redis==5.4.0",
|
"django-redis==5.4.0",
|
||||||
"django-storages[s3]==1.14.4",
|
"django-storages[s3]==1.14.4",
|
||||||
"django-timezone-field>=5.1",
|
"django-timezone-field>=5.1",
|
||||||
"django==5.1",
|
"django==5.1.1",
|
||||||
"djangorestframework==3.15.2",
|
"djangorestframework==3.15.2",
|
||||||
"drf_spectacular==0.27.2",
|
"drf_spectacular==0.27.2",
|
||||||
"dockerflow==2024.4.2",
|
"dockerflow==2024.4.2",
|
||||||
"easy_thumbnails==2.9",
|
"easy_thumbnails==2.10",
|
||||||
"factory_boy==3.3.1",
|
"factory_boy==3.3.1",
|
||||||
"freezegun==1.5.1",
|
"freezegun==1.5.1",
|
||||||
"gunicorn==23.0.0",
|
"gunicorn==23.0.0",
|
||||||
@@ -48,11 +48,11 @@ dependencies = [
|
|||||||
"june-analytics-python==2.3.0",
|
"june-analytics-python==2.3.0",
|
||||||
"markdown==3.7",
|
"markdown==3.7",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.5.0",
|
||||||
"psycopg[binary]==3.2.1",
|
"psycopg[binary]==3.2.2",
|
||||||
"PyJWT==2.9.0",
|
"PyJWT==2.9.0",
|
||||||
"python-frontmatter==1.1.0",
|
"python-frontmatter==1.1.0",
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
"sentry-sdk==2.13.0",
|
"sentry-sdk==2.14.0",
|
||||||
"url-normalize==1.4.3",
|
"url-normalize==1.4.3",
|
||||||
"WeasyPrint>=60.2",
|
"WeasyPrint>=60.2",
|
||||||
"whitenoise==6.7.0",
|
"whitenoise==6.7.0",
|
||||||
@@ -76,13 +76,13 @@ dev = [
|
|||||||
"pylint-django==2.5.5",
|
"pylint-django==2.5.5",
|
||||||
"pylint==3.2.7",
|
"pylint==3.2.7",
|
||||||
"pytest-cov==5.0.0",
|
"pytest-cov==5.0.0",
|
||||||
"pytest-django==4.8.0",
|
"pytest-django==4.9.0",
|
||||||
"pytest==8.3.2",
|
"pytest==8.3.3",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.6.1",
|
"pytest-xdist==3.6.1",
|
||||||
"responses==0.25.3",
|
"responses==0.25.3",
|
||||||
"ruff==0.6.3",
|
"ruff==0.6.5",
|
||||||
"types-requests==2.32.0.20240712",
|
"types-requests==2.32.0.20240914",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Meet</title>
|
<title>Visio</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+2319
-2608
File diff suppressed because it is too large
Load Diff
+30
-27
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
@@ -13,45 +13,48 @@
|
|||||||
"check": "prettier --check ./src"
|
"check": "prettier --check ./src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/components-react": "2.3.3",
|
"@livekit/components-react": "2.5.4",
|
||||||
"@livekit/components-styles": "1.0.12",
|
"@livekit/components-styles": "1.1.2",
|
||||||
"@pandacss/preset-panda": "0.41.0",
|
"@livekit/track-processors": "0.3.2",
|
||||||
|
"@pandacss/preset-panda": "0.46.0",
|
||||||
"@react-aria/toast": "3.0.0-beta.15",
|
"@react-aria/toast": "3.0.0-beta.15",
|
||||||
"@remixicon/react": "4.2.0",
|
"@remixicon/react": "4.2.0",
|
||||||
"@tanstack/react-query": "5.49.2",
|
"@tanstack/react-query": "5.56.2",
|
||||||
|
"crisp-sdk-web": "1.0.25",
|
||||||
"hoofd": "1.7.1",
|
"hoofd": "1.7.1",
|
||||||
"i18next": "23.12.1",
|
"i18next": "23.15.1",
|
||||||
"i18next-browser-languagedetector": "8.0.0",
|
"i18next-browser-languagedetector": "8.0.0",
|
||||||
"i18next-parser": "9.0.0",
|
"i18next-parser": "9.0.2",
|
||||||
"i18next-resources-to-backend": "1.2.1",
|
"i18next-resources-to-backend": "1.2.1",
|
||||||
"livekit-client": "2.3.1",
|
"livekit-client": "2.5.1",
|
||||||
"react": "18.2.0",
|
"posthog-js": "1.161.5",
|
||||||
"react-aria-components": "1.2.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-aria-components": "1.3.3",
|
||||||
"react-i18next": "14.1.3",
|
"react-dom": "18.3.1",
|
||||||
|
"react-i18next": "15.0.2",
|
||||||
"use-sound": "4.0.3",
|
"use-sound": "4.0.3",
|
||||||
"valtio": "1.13.2",
|
"valtio": "2.0.0",
|
||||||
"wouter": "3.3.0"
|
"wouter": "3.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pandacss/dev": "0.41.0",
|
"@pandacss/dev": "0.46.0",
|
||||||
"@tanstack/eslint-plugin-query": "5.49.1",
|
"@tanstack/eslint-plugin-query": "5.56.1",
|
||||||
"@tanstack/react-query-devtools": "5.49.2",
|
"@tanstack/react-query-devtools": "5.56.2",
|
||||||
"@types/node": "20.14.9",
|
"@types/node": "20.16.5",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.5",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
"@typescript-eslint/eslint-plugin": "8.5.0",
|
||||||
"@typescript-eslint/parser": "7.13.1",
|
"@typescript-eslint/parser": "8.5.0",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.1",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
"eslint-plugin-jsx-a11y": "6.10.0",
|
||||||
"eslint-plugin-react-hooks": "4.6.2",
|
"eslint-plugin-react-hooks": "4.6.2",
|
||||||
"eslint-plugin-react-refresh": "0.4.7",
|
"eslint-plugin-react-refresh": "0.4.12",
|
||||||
"postcss": "8.4.39",
|
"postcss": "8.4.47",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"typescript": "5.5.2",
|
"typescript": "5.6.2",
|
||||||
"vite": "5.3.1",
|
"vite": "5.4.5",
|
||||||
"vite-tsconfig-paths": "4.3.2"
|
"vite-tsconfig-paths": "5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { routes } from './routes'
|
|||||||
import './i18n/init'
|
import './i18n/init'
|
||||||
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
|
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
|
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||||
|
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -21,6 +23,9 @@ function App() {
|
|||||||
const isProduction = import.meta.env.PROD
|
const isProduction = import.meta.env.PROD
|
||||||
silenceLiveKitLogs(isProduction)
|
silenceLiveKitLogs(isProduction)
|
||||||
|
|
||||||
|
useAnalytics()
|
||||||
|
useSupport()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useLocation } from 'wouter'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
|
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
|
|
||||||
|
const ANALYTICS_ID = 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom'
|
||||||
|
const ANALYTICS_HOST = 'https://eu.i.posthog.com'
|
||||||
|
|
||||||
|
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 const useAnalytics = () => {
|
||||||
|
const [location] = useLocation()
|
||||||
|
|
||||||
|
const isProduction = import.meta.env.PROD
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// We're on a free tier, so we need to limit the number of events we send to PostHog.
|
||||||
|
// Be frugal with event tracking, even though we could filter them out later if necessary.
|
||||||
|
if (!isProduction) return
|
||||||
|
if (posthog.__loaded) return
|
||||||
|
posthog.init(ANALYTICS_ID, {
|
||||||
|
api_host: ANALYTICS_HOST,
|
||||||
|
person_profiles: 'always',
|
||||||
|
})
|
||||||
|
}, [isProduction])
|
||||||
|
|
||||||
|
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
|
||||||
|
useEffect(() => {
|
||||||
|
posthog.capture('$pageview')
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { fetchUser } from './fetchUser'
|
import { fetchUser } from './fetchUser'
|
||||||
import { type ApiUser } from './ApiUser'
|
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
|
* `isLoggedIn` is undefined while query is loading and true/false when it's done
|
||||||
*/
|
*/
|
||||||
@@ -15,6 +18,13 @@ export const useUser = () => {
|
|||||||
staleTime: 1000 * 60 * 60, // 1 hour
|
staleTime: 1000 * 60 * 60, // 1 hour
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query?.data) {
|
||||||
|
startAnalyticsSession(query.data)
|
||||||
|
initializeSupportSession(query.data)
|
||||||
|
}
|
||||||
|
}, [query.data])
|
||||||
|
|
||||||
const isLoggedIn =
|
const isLoggedIn =
|
||||||
query.status === 'success' ? query.data !== false : undefined
|
query.status === 'success' ? query.data !== false : undefined
|
||||||
const isLoggedOut = isLoggedIn === false
|
const isLoggedOut = isLoggedIn === false
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
|
|||||||
name="roomId"
|
name="roomId"
|
||||||
label={t('joinInputLabel')}
|
label={t('joinInputLabel')}
|
||||||
description={t('joinInputExample', {
|
description={t('joinInputExample', {
|
||||||
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
|
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
|
||||||
})}
|
})}
|
||||||
validate={(value) => {
|
validate={(value) => {
|
||||||
return !isRoomValid(value.trim()) ? (
|
return !isRoomValid(value.trim()) ? (
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const LaterMeetingDialog = ({
|
|||||||
setIsCopied(true)
|
setIsCopied(true)
|
||||||
}}
|
}}
|
||||||
onHoverChange={setIsHovered}
|
onHoverChange={setIsHovered}
|
||||||
|
data-attr="later-dialog-copy"
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { DialogTrigger } from 'react-aria-components'
|
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
|
||||||
import { Button, Menu, Text } from '@/primitives'
|
import { Button, Menu, Text } from '@/primitives'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { Centered } from '@/layout/Centered'
|
import { Centered } from '@/layout/Centered'
|
||||||
import { generateRoomId } from '@/features/rooms'
|
import { generateRoomId } from '@/features/rooms'
|
||||||
import { authUrl, useUser, UserAware } from '@/features/auth'
|
import { useUser, UserAware } from '@/features/auth'
|
||||||
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
import { useCreateRoom } from '@/features/rooms'
|
import { useCreateRoom } from '@/features/rooms'
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||||
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
||||||
import { RiAddLine, RiLink } from '@remixicon/react'
|
import { RiAddLine, RiLink } from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
|
|
||||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
@@ -42,10 +42,12 @@ export const Home = () => {
|
|||||||
{t('loginToCreateMeeting')}
|
{t('loginToCreateMeeting')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<HStack gap="gutter">
|
<HStack gap="gutter" alignItems="start">
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Button variant="primary">{t('createMeeting')}</Button>
|
<Button variant="primary" data-attr="create-meeting">
|
||||||
|
{t('createMeeting')}
|
||||||
|
</Button>
|
||||||
<RACMenu>
|
<RACMenu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuItemRecipe({ icon: true })}
|
className={menuItemRecipe({ icon: true })}
|
||||||
@@ -57,6 +59,7 @@ export const Home = () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
data-attr="create-option-instant"
|
||||||
>
|
>
|
||||||
<RiAddLine size={18} />
|
<RiAddLine size={18} />
|
||||||
{t('createMenu.instantOption')}
|
{t('createMenu.instantOption')}
|
||||||
@@ -69,6 +72,7 @@ export const Home = () => {
|
|||||||
setLaterRoomId(data.slug)
|
setLaterRoomId(data.slug)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
data-attr="create-option-later"
|
||||||
>
|
>
|
||||||
<RiLink size={18} />
|
<RiLink size={18} />
|
||||||
{t('createMenu.laterOption')}
|
{t('createMenu.laterOption')}
|
||||||
@@ -76,12 +80,16 @@ export const Home = () => {
|
|||||||
</RACMenu>
|
</RACMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="primary" href={authUrl()}>
|
<ProConnectButton />
|
||||||
{t('login', { ns: 'global' })}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="primary" outline>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
outline
|
||||||
|
style={{
|
||||||
|
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('joinMeeting')}
|
{t('joinMeeting')}
|
||||||
</Button>
|
</Button>
|
||||||
<JoinMeetingDialog />
|
<JoinMeetingDialog />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
|
import { Participant, RoomEvent } from 'livekit-client'
|
||||||
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
||||||
import { NotificationType } from './NotificationType'
|
import { NotificationType } from './NotificationType'
|
||||||
import { Div } from '@/primitives'
|
import { Div } from '@/primitives'
|
||||||
@@ -50,31 +50,32 @@ export const MainNotificationToast = () => {
|
|||||||
}
|
}
|
||||||
}, [room])
|
}, [room])
|
||||||
|
|
||||||
// fixme - close all related toasters when hands are lowered remotely
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const decoder = new TextDecoder()
|
|
||||||
|
|
||||||
const handleNotificationReceived = (
|
const handleNotificationReceived = (
|
||||||
payload: Uint8Array,
|
prevMetadataStr: string | undefined,
|
||||||
participant?: RemoteParticipant
|
participant: Participant
|
||||||
) => {
|
) => {
|
||||||
if (!participant) {
|
if (!participant) return
|
||||||
return
|
if (isMobileBrowser()) return
|
||||||
}
|
if (participant.isLocal) return
|
||||||
if (isMobileBrowser()) {
|
|
||||||
return
|
const prevMetadata = JSON.parse(prevMetadataStr || '{}')
|
||||||
}
|
const metadata = JSON.parse(participant.metadata || '{}')
|
||||||
const notification = decoder.decode(payload)
|
|
||||||
|
if (prevMetadata.raised == metadata.raised) return
|
||||||
|
|
||||||
const existingToast = toastQueue.visibleToasts.find(
|
const existingToast = toastQueue.visibleToasts.find(
|
||||||
(toast) =>
|
(toast) =>
|
||||||
toast.content.participant === participant &&
|
toast.content.participant === participant &&
|
||||||
toast.content.type === NotificationType.Raised
|
toast.content.type === NotificationType.Raised
|
||||||
)
|
)
|
||||||
if (existingToast && notification === NotificationType.Lowered) {
|
|
||||||
|
if (existingToast && prevMetadata.raised && !metadata.raised) {
|
||||||
toastQueue.close(existingToast.key)
|
toastQueue.close(existingToast.key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!existingToast && notification === NotificationType.Raised) {
|
|
||||||
|
if (!existingToast && !prevMetadata.raised && metadata.raised) {
|
||||||
triggerNotificationSound(NotificationType.Raised)
|
triggerNotificationSound(NotificationType.Raised)
|
||||||
toastQueue.add(
|
toastQueue.add(
|
||||||
{
|
{
|
||||||
@@ -86,13 +87,23 @@ export const MainNotificationToast = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
room.on(RoomEvent.DataReceived, handleNotificationReceived)
|
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
room.off(RoomEvent.DataReceived, handleNotificationReceived)
|
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
|
||||||
}
|
}
|
||||||
}, [room, triggerNotificationSound])
|
}, [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 (
|
return (
|
||||||
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
|
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
|
||||||
<ToastProvider />
|
<ToastProvider />
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { Room, RoomOptions } from 'livekit-client'
|
import { Room, RoomOptions } from 'livekit-client'
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { QueryAware } from '@/components/QueryAware'
|
import { QueryAware } from '@/components/QueryAware'
|
||||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||||
@@ -19,6 +18,7 @@ import { useCreateRoom } from '../api/createRoom'
|
|||||||
import { InviteDialog } from './InviteDialog'
|
import { InviteDialog } from './InviteDialog'
|
||||||
|
|
||||||
import { VideoConference } from '../livekit/prefabs/VideoConference'
|
import { VideoConference } from '../livekit/prefabs/VideoConference'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -31,6 +31,9 @@ export const Conference = ({
|
|||||||
mode?: 'join' | 'create'
|
mode?: 'join' | 'create'
|
||||||
initialRoomData?: ApiRoom
|
initialRoomData?: ApiRoom
|
||||||
}) => {
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
posthog.capture('visit-room', { slug: roomId })
|
||||||
|
}, [roomId])
|
||||||
const fetchKey = [keys.room, roomId, userConfig.username]
|
const fetchKey = [keys.room, roomId, userConfig.username]
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -79,25 +82,6 @@ export const Conference = ({
|
|||||||
|
|
||||||
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
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')
|
const { t } = useTranslation('rooms')
|
||||||
if (isCreateError) {
|
if (isCreateError) {
|
||||||
// this error screen should be replaced by a proper waiting room for anonymous user.
|
// this error screen should be replaced by a proper waiting room for anonymous user.
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const InviteDialog = ({
|
|||||||
setIsCopied(true)
|
setIsCopied(true)
|
||||||
}}
|
}}
|
||||||
onHoverChange={setIsHovered}
|
onHoverChange={setIsHovered}
|
||||||
|
data-attr="share-dialog-copy"
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export const Join = ({
|
|||||||
<PreJoin
|
<PreJoin
|
||||||
persistUserChoices
|
persistUserChoices
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
micLabel={t('join.micLabel')}
|
micLabel={t('join.audioinput.label')}
|
||||||
camLabel={t('join.camlabel')}
|
camLabel={t('join.videoinput.label')}
|
||||||
joinLabel={t('join.joinLabel')}
|
joinLabel={t('join.joinLabel')}
|
||||||
userLabel={t('join.userLabel')}
|
userLabel={t('join.userLabel')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const useLowerHandParticipants = () => {
|
|||||||
)
|
)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('An error occurred while lowering hands :', error)
|
||||||
throw new Error('An error occurred while lowering hands.')
|
throw new Error('An error occurred while lowering hands.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { css } from '@/styled-system/css'
|
|||||||
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
|
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
|
||||||
|
|
||||||
export const ChatToggle = () => {
|
export const ChatToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
|
||||||
|
|
||||||
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
|
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
|
||||||
const tooltipLabel = isChatOpen ? 'open' : 'closed'
|
const tooltipLabel = isChatOpen ? 'open' : 'closed'
|
||||||
@@ -20,10 +20,11 @@ export const ChatToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.chat.${tooltipLabel}`)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(`controls.chat.${tooltipLabel}`)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isChatOpen}
|
isSelected={isChatOpen}
|
||||||
onPress={() => toggleChat()}
|
onPress={() => toggleChat()}
|
||||||
|
data-attr={`controls-chat-${tooltipLabel}`}
|
||||||
>
|
>
|
||||||
<RiChat1Line />
|
<RiChat1Line />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -4,30 +4,16 @@ import { ToggleButton } from '@/primitives'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
|
||||||
|
|
||||||
export const HandToggle = () => {
|
export const HandToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||||
|
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||||
participant: room.localParticipant,
|
participant: room.localParticipant,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = isHandRaised
|
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||||
? t('controls.hand.lower')
|
|
||||||
: t('controls.hand.raise')
|
|
||||||
|
|
||||||
const notifyOtherParticipants = (isHandRaised: boolean) => {
|
|
||||||
room.localParticipant.publishData(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
!isHandRaised ? NotificationType.Raised : NotificationType.Lowered
|
|
||||||
),
|
|
||||||
{
|
|
||||||
reliable: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -39,13 +25,11 @@ export const HandToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={label}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={label}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isHandRaised}
|
isSelected={isHandRaised}
|
||||||
onPress={() => {
|
onPress={() => toggleRaisedHand()}
|
||||||
notifyOtherParticipants(isHandRaised)
|
data-attr={`controls-hand-${tooltipLabel}`}
|
||||||
toggleRaisedHand()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+42
-27
@@ -1,13 +1,16 @@
|
|||||||
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
||||||
import {
|
import {
|
||||||
|
RiAccountBoxLine,
|
||||||
RiFeedbackLine,
|
RiFeedbackLine,
|
||||||
RiQuestionLine,
|
RiQuestionLine,
|
||||||
RiSettings3Line,
|
RiSettings3Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
|
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dispatch, SetStateAction } from 'react'
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
|
import { DialogState } from './OptionsButton'
|
||||||
|
import { Separator } from '@/primitives/Separator'
|
||||||
|
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
|
||||||
|
|
||||||
// @todo try refactoring it to use MenuList component
|
// @todo try refactoring it to use MenuList component
|
||||||
export const OptionsMenuItems = ({
|
export const OptionsMenuItems = ({
|
||||||
@@ -15,8 +18,8 @@ export const OptionsMenuItems = ({
|
|||||||
}: {
|
}: {
|
||||||
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
|
const { toggleEffects } = useWidgetInteraction()
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
style={{
|
style={{
|
||||||
@@ -24,29 +27,41 @@ export const OptionsMenuItems = ({
|
|||||||
width: '300px',
|
width: '300px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<Section>
|
||||||
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
|
<MenuItem
|
||||||
target="_blank"
|
onAction={() => toggleEffects()}
|
||||||
className={menuItemRecipe({ icon: true })}
|
className={menuItemRecipe({ icon: true })}
|
||||||
>
|
>
|
||||||
<RiQuestionLine size={18} />
|
<RiAccountBoxLine size={20} />
|
||||||
{t('options.items.support')}
|
{t('effects')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
</Section>
|
||||||
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
<Separator />
|
||||||
target="_blank"
|
<Section>
|
||||||
className={menuItemRecipe({ icon: true })}
|
<MenuItem
|
||||||
>
|
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
|
||||||
<RiFeedbackLine size={18} />
|
target="_blank"
|
||||||
{t('options.items.feedbacks')}
|
className={menuItemRecipe({ icon: true })}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
<RiQuestionLine size={20} />
|
||||||
className={menuItemRecipe({ icon: true })}
|
{t('support')}
|
||||||
onAction={() => onOpenDialog('settings')}
|
</MenuItem>
|
||||||
>
|
<MenuItem
|
||||||
<RiSettings3Line size={18} />
|
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||||
{t('options.items.settings')}
|
target="_blank"
|
||||||
</MenuItem>
|
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>
|
||||||
</RACMenu>
|
</RACMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -73,6 +73,7 @@ export const HandRaisedListItem = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => lowerHandParticipant(participant)}
|
onPress={() => lowerHandParticipant(participant)}
|
||||||
tooltip={t('participants.lowerParticipantHand', { name })}
|
tooltip={t('participants.lowerParticipantHand', { name })}
|
||||||
|
data-attr="participants-lower-hand"
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ export const LowerAllHandsButton = ({
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="text"
|
variant="text"
|
||||||
onPress={() => lowerHandParticipants(participants)}
|
onPress={() => lowerHandParticipants(participants)}
|
||||||
|
data-attr="participants-lower-hands"
|
||||||
>
|
>
|
||||||
{t('participants.lowerParticipantsHand')}
|
{t('participants.lowerParticipantsHand')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
@@ -78,6 +78,7 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
|||||||
? muteParticipant(participant)
|
? muteParticipant(participant)
|
||||||
: setIsAlertOpen(true)
|
: setIsAlertOpen(true)
|
||||||
}
|
}
|
||||||
|
data-attr="participants-mute"
|
||||||
>
|
>
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<RiMicOffFill color={'gray'} />
|
<RiMicOffFill color={'gray'} />
|
||||||
|
|||||||
+45
-80
@@ -1,27 +1,27 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
import { allParticipantRoomEvents } from '@livekit/components-core'
|
||||||
import { useParticipants } from '@livekit/components-react'
|
import { useParticipants } from '@livekit/components-react'
|
||||||
|
|
||||||
import { Heading } from 'react-aria-components'
|
import { Div, H } from '@/primitives'
|
||||||
import { Box, Button, Div, H } from '@/primitives'
|
|
||||||
import { text } from '@/primitives/Text'
|
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
|
||||||
import { participantsStore } from '@/stores/participants'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
|
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
|
||||||
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
|
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
|
||||||
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
|
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
|
||||||
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
|
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
|
||||||
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton'
|
import { RoomEvent } from 'livekit-client'
|
||||||
|
|
||||||
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
||||||
export const ParticipantsList = () => {
|
export const ParticipantsList = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
|
||||||
|
|
||||||
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
|
// 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
|
// 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.
|
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
|
||||||
const participants = useParticipants({
|
const participants = useParticipants({
|
||||||
updateOnlyOn: allParticipantRoomEvents,
|
updateOnlyOn: [
|
||||||
|
RoomEvent.ParticipantNameChanged,
|
||||||
|
...allParticipantRoomEvents,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortedRemoteParticipants = participants
|
const sortedRemoteParticipants = participants
|
||||||
@@ -44,75 +44,40 @@ export const ParticipantsList = () => {
|
|||||||
|
|
||||||
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
|
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
size="sm"
|
<H
|
||||||
minWidth="360px"
|
lvl={2}
|
||||||
className={css({
|
className={css({
|
||||||
overflow: 'hidden',
|
fontSize: '0.875rem',
|
||||||
display: 'flex',
|
fontWeight: 'bold',
|
||||||
flexDirection: 'column',
|
color: '#5f6368',
|
||||||
margin: '1.5rem 1.5rem 1.5rem 0',
|
padding: '0 1.5rem',
|
||||||
padding: 0,
|
marginBottom: '0.83em',
|
||||||
gap: 0,
|
})}
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
slot="title"
|
|
||||||
level={1}
|
|
||||||
className={text({ variant: 'h2' })}
|
|
||||||
style={{
|
|
||||||
paddingLeft: '1.5rem',
|
|
||||||
paddingTop: '1rem',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('participants.heading')}
|
{t('subheading').toUpperCase()}
|
||||||
</Heading>
|
</H>
|
||||||
<Div position="absolute" top="5" right="5">
|
{raisedHandParticipants.length > 0 && (
|
||||||
<Button
|
<Div marginBottom=".9375rem">
|
||||||
invisible
|
<ParticipantsCollapsableList
|
||||||
size="xs"
|
heading={t('raisedHands')}
|
||||||
onPress={() => (participantsStore.showParticipants = false)}
|
participants={raisedHandParticipants}
|
||||||
aria-label={t('participants.closeButton')}
|
renderParticipant={(participant) => (
|
||||||
tooltip={t('participants.closeButton')}
|
<HandRaisedListItem participant={participant} />
|
||||||
>
|
)}
|
||||||
<RiCloseLine />
|
action={() => (
|
||||||
</Button>
|
<LowerAllHandsButton participants={raisedHandParticipants} />
|
||||||
</Div>
|
)}
|
||||||
<Div overflowY="scroll">
|
/>
|
||||||
<H
|
</Div>
|
||||||
lvl={2}
|
)}
|
||||||
className={css({
|
<ParticipantsCollapsableList
|
||||||
fontSize: '0.875rem',
|
heading={t('contributors')}
|
||||||
fontWeight: 'bold',
|
participants={sortedParticipants}
|
||||||
color: '#5f6368',
|
renderParticipant={(participant) => (
|
||||||
padding: '0 1.5rem',
|
<ParticipantListItem participant={participant} />
|
||||||
marginBottom: '0.83em',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('participants.subheading').toUpperCase()}
|
|
||||||
</H>
|
|
||||||
{raisedHandParticipants.length > 0 && (
|
|
||||||
<Div marginBottom=".9375rem">
|
|
||||||
<ParticipantsCollapsableList
|
|
||||||
heading={t('participants.raisedHands')}
|
|
||||||
participants={raisedHandParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<HandRaisedListItem participant={participant} />
|
|
||||||
)}
|
|
||||||
action={() => (
|
|
||||||
<LowerAllHandsButton participants={raisedHandParticipants} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
)}
|
)}
|
||||||
<ParticipantsCollapsableList
|
/>
|
||||||
heading={t('participants.contributors')}
|
</>
|
||||||
participants={sortedParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<ParticipantListItem participant={participant} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -6,7 +6,7 @@ import { useParticipants } from '@livekit/components-react'
|
|||||||
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
|
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
|
||||||
|
|
||||||
export const ParticipantsToggle = () => {
|
export const ParticipantsToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context could not be used due to inconsistent refresh behavior.
|
* Context could not be used due to inconsistent refresh behavior.
|
||||||
@@ -30,10 +30,11 @@ export const ParticipantsToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.participants.${tooltipLabel}`)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(`controls.participants.${tooltipLabel}`)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isParticipantsOpen}
|
isSelected={isParticipantsOpen}
|
||||||
onPress={() => toggleParticipants()}
|
onPress={() => toggleParticipants()}
|
||||||
|
data-attr={`controls-participants-${tooltipLabel}`}
|
||||||
>
|
>
|
||||||
<RiGroupLine />
|
<RiGroupLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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,
|
|
||||||
]
|
|
||||||
@@ -7,6 +7,7 @@ type useRaisedHandProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRaisedHand({ participant }: useRaisedHandProps) {
|
export function useRaisedHand({ participant }: useRaisedHandProps) {
|
||||||
|
// fixme - refactor this part to rely on attributes
|
||||||
const { metadata } = useParticipantInfo({ participant })
|
const { metadata } = useParticipantInfo({ participant })
|
||||||
const parsedMetadata = JSON.parse(metadata || '{}')
|
const parsedMetadata = JSON.parse(metadata || '{}')
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
import { useLayoutContext } from '@livekit/components-react'
|
import { useLayoutContext } from '@livekit/components-react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { participantsStore } from '@/stores/participants.ts'
|
import { layoutStore } from '@/stores/layout'
|
||||||
|
|
||||||
export const useWidgetInteraction = () => {
|
export const useWidgetInteraction = () => {
|
||||||
const { dispatch, state } = useLayoutContext().widget
|
const { dispatch, state } = useLayoutContext().widget
|
||||||
|
|
||||||
const participantsSnap = useSnapshot(participantsStore)
|
const layoutSnap = useSnapshot(layoutStore)
|
||||||
const isParticipantsOpen = participantsSnap.showParticipants
|
const sidePanel = layoutSnap.sidePanel
|
||||||
|
|
||||||
|
const isParticipantsOpen = sidePanel == 'participants'
|
||||||
|
const isEffectsOpen = sidePanel == 'effects'
|
||||||
|
|
||||||
const toggleParticipants = () => {
|
const toggleParticipants = () => {
|
||||||
if (dispatch && state?.showChat) {
|
if (dispatch && state?.showChat) {
|
||||||
dispatch({ msg: 'toggle_chat' })
|
dispatch({ msg: 'toggle_chat' })
|
||||||
}
|
}
|
||||||
participantsStore.showParticipants = !isParticipantsOpen
|
layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleChat = () => {
|
const toggleChat = () => {
|
||||||
if (isParticipantsOpen) {
|
if (isParticipantsOpen) {
|
||||||
participantsStore.showParticipants = false
|
layoutStore.sidePanel = null
|
||||||
}
|
}
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
dispatch({ msg: 'toggle_chat' })
|
dispatch({ msg: 'toggle_chat' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleEffects = () => {
|
||||||
|
if (dispatch && state?.showChat) {
|
||||||
|
dispatch({ msg: 'toggle_chat' })
|
||||||
|
}
|
||||||
|
layoutStore.sidePanel = isEffectsOpen ? null : 'effects'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
toggleChat,
|
toggleChat,
|
||||||
|
toggleEffects,
|
||||||
isChatOpen: state?.showChat,
|
isChatOpen: state?.showChat,
|
||||||
unreadMessages: state?.unreadMessages,
|
unreadMessages: state?.unreadMessages,
|
||||||
isParticipantsOpen,
|
isParticipantsOpen,
|
||||||
|
isEffectsOpen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import * as React from 'react'
|
|||||||
import { supportsScreenSharing } from '@livekit/components-core'
|
import { supportsScreenSharing } from '@livekit/components-core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DisconnectButton,
|
|
||||||
LeaveIcon,
|
|
||||||
MediaDeviceMenu,
|
|
||||||
TrackToggle,
|
|
||||||
useMaybeLayoutContext,
|
useMaybeLayoutContext,
|
||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
@@ -15,11 +11,13 @@ import {
|
|||||||
import { mergeProps } from '@/utils/mergeProps.ts'
|
import { mergeProps } from '@/utils/mergeProps.ts'
|
||||||
import { StartMediaButton } from '../components/controls/StartMediaButton'
|
import { StartMediaButton } from '../components/controls/StartMediaButton'
|
||||||
import { useMediaQuery } from '../hooks/useMediaQuery'
|
import { useMediaQuery } from '../hooks/useMediaQuery'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
||||||
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
|
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
|
||||||
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
|
import { ChatToggle } from '../components/controls/ChatToggle'
|
||||||
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
|
import { HandToggle } from '../components/controls/HandToggle'
|
||||||
|
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
|
||||||
|
import { LeaveButton } from '../components/controls/LeaveButton'
|
||||||
|
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type ControlBarControls = {
|
export type ControlBarControls = {
|
||||||
@@ -38,7 +36,7 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
controls?: ControlBarControls
|
controls?: ControlBarControls
|
||||||
/**
|
/**
|
||||||
* If `true`, the user's device choices will be persisted.
|
* If `true`, the user's device choices will be persisted.
|
||||||
* This will enables the user to have the same device choices when they rejoin the room.
|
* This will enable the user to have the same device choices when they rejoin the room.
|
||||||
* @defaultValue true
|
* @defaultValue true
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
@@ -67,7 +65,6 @@ export function ControlBar({
|
|||||||
onDeviceError,
|
onDeviceError,
|
||||||
...props
|
...props
|
||||||
}: ControlBarProps) {
|
}: ControlBarProps) {
|
||||||
const { t } = useTranslation('rooms')
|
|
||||||
const [isChatOpen, setIsChatOpen] = React.useState(false)
|
const [isChatOpen, setIsChatOpen] = React.useState(false)
|
||||||
const layoutContext = useMaybeLayoutContext()
|
const layoutContext = useMaybeLayoutContext()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -83,26 +80,8 @@ export function ControlBar({
|
|||||||
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
|
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
|
||||||
variation ??= defaultVariation
|
variation ??= defaultVariation
|
||||||
|
|
||||||
const showIcon = React.useMemo(
|
|
||||||
() => variation === 'minimal' || variation === 'verbose',
|
|
||||||
[variation]
|
|
||||||
)
|
|
||||||
const showText = React.useMemo(
|
|
||||||
() => variation === 'textOnly' || variation === 'verbose',
|
|
||||||
[variation]
|
|
||||||
)
|
|
||||||
|
|
||||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
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 htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -126,72 +105,38 @@ export function ControlBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...htmlProps}>
|
<div {...htmlProps}>
|
||||||
<div className="lk-button-group">
|
<SelectToggleDevice
|
||||||
<TrackToggle
|
source={Track.Source.Microphone}
|
||||||
source={Track.Source.Microphone}
|
onChange={microphoneOnChange}
|
||||||
showIcon={showIcon}
|
onDeviceError={(error) =>
|
||||||
onChange={microphoneOnChange}
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
onDeviceError={(error) =>
|
}
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
onActiveDeviceChange={(deviceId) =>
|
||||||
}
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
>
|
}
|
||||||
{showText && t('controls.microphone')}
|
/>
|
||||||
</TrackToggle>
|
<SelectToggleDevice
|
||||||
<div className="lk-button-group-menu">
|
source={Track.Source.Camera}
|
||||||
<MediaDeviceMenu
|
onChange={cameraOnChange}
|
||||||
kind="audioinput"
|
onDeviceError={(error) =>
|
||||||
onActiveDeviceChange={(_kind, deviceId) =>
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
}
|
||||||
}
|
onActiveDeviceChange={(deviceId) =>
|
||||||
/>
|
saveVideoInputDeviceId(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 && (
|
{browserSupportsScreenSharing && (
|
||||||
<TrackToggle
|
<ScreenShareToggle
|
||||||
source={Track.Source.ScreenShare}
|
|
||||||
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
|
|
||||||
showIcon={showIcon}
|
|
||||||
onChange={onScreenShareChange}
|
|
||||||
onDeviceError={(error) =>
|
onDeviceError={(error) =>
|
||||||
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
{showText &&
|
|
||||||
t(
|
|
||||||
isScreenShareEnabled
|
|
||||||
? 'controls.stopScreenShare'
|
|
||||||
: 'controls.shareScreen'
|
|
||||||
)}
|
|
||||||
</TrackToggle>
|
|
||||||
)}
|
)}
|
||||||
<HandToggle />
|
<HandToggle />
|
||||||
<ChatToggle />
|
<ChatToggle />
|
||||||
<ParticipantsToggle />
|
<ParticipantsToggle />
|
||||||
<OptionsButton />
|
<OptionsButton />
|
||||||
<DisconnectButton>
|
<LeaveButton />
|
||||||
{showIcon && <LeaveIcon />}
|
|
||||||
{showText && t('controls.leave')}
|
|
||||||
</DisconnectButton>
|
|
||||||
<StartMediaButton />
|
<StartMediaButton />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ import {
|
|||||||
import { ControlBar } from './ControlBar'
|
import { ControlBar } from './ControlBar'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { cva } from '@/styled-system/css'
|
import { cva } from '@/styled-system/css'
|
||||||
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
|
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { participantsStore } from '@/stores/participants'
|
import { layoutStore } from '@/stores/layout'
|
||||||
import { FocusLayout } from '../components/FocusLayout'
|
import { FocusLayout } from '../components/FocusLayout'
|
||||||
import { ParticipantTile } from '../components/ParticipantTile'
|
import { ParticipantTile } from '../components/ParticipantTile'
|
||||||
|
import { SidePanel } from '../components/SidePanel'
|
||||||
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
||||||
|
|
||||||
const LayoutWrapper = styled(
|
const LayoutWrapper = styled(
|
||||||
@@ -172,8 +172,8 @@ export function VideoConference({
|
|||||||
])
|
])
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
const participantsSnap = useSnapshot(participantsStore)
|
const layoutSnap = useSnapshot(layoutStore)
|
||||||
const showParticipants = participantsSnap.showParticipants
|
const sidePanel = layoutSnap.sidePanel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lk-video-conference" {...props}>
|
<div className="lk-video-conference" {...props}>
|
||||||
@@ -223,7 +223,7 @@ export function VideoConference({
|
|||||||
messageEncoder={chatMessageEncoder}
|
messageEncoder={chatMessageEncoder}
|
||||||
messageDecoder={chatMessageDecoder}
|
messageDecoder={chatMessageDecoder}
|
||||||
/>
|
/>
|
||||||
{showParticipants && <ParticipantsList />}
|
{sidePanel && <SidePanel />}
|
||||||
</LayoutWrapper>
|
</LayoutWrapper>
|
||||||
<ControlBar />
|
<ControlBar />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
|
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
|
||||||
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
|
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
|
|
||||||
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
|
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
|
||||||
|
|
||||||
@@ -28,9 +29,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<P>{t('account.youAreNotLoggedIn')}</P>
|
<P>{t('account.youAreNotLoggedIn')}</P>
|
||||||
<P>
|
<ProConnectButton />
|
||||||
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
|
|
||||||
</P>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<H lvl={2}>{t('language.heading')}</H>
|
<H lvl={2}>{t('language.heading')}</H>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
useRoomContext,
|
useRoomContext,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
|
|
||||||
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
Pick<TabPanelProps, 'id'>
|
Pick<TabPanelProps, 'id'>
|
||||||
@@ -58,9 +59,7 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<P>{t('account.youAreNotLoggedIn')}</P>
|
<P>{t('account.youAreNotLoggedIn')}</P>
|
||||||
<P>
|
<ProConnectButton />
|
||||||
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
|
|
||||||
</P>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<HStack
|
<HStack
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Crisp } from 'crisp-sdk-web'
|
||||||
|
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
|
|
||||||
|
const SUPPORT_ID = '58ea6697-8eba-4492-bc59-ad6562585041'
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure Crisp chat for real-time support across all pages.
|
||||||
|
export const useSupport = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!SUPPORT_ID) {
|
||||||
|
console.warn('Crisp Website ID is not set')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Crisp.isCrispInjected()) return
|
||||||
|
Crisp.configure(SUPPORT_ID)
|
||||||
|
Crisp.setHideOnMobile(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -2,13 +2,16 @@ import { Link } from 'wouter'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { Stack } from '@/styled-system/jsx'
|
import { Stack } from '@/styled-system/jsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { A, Text, Button } from '@/primitives'
|
import { Text, Button } from '@/primitives'
|
||||||
import { SettingsButton } from '@/features/settings'
|
import { SettingsButton } from '@/features/settings'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||||
import { Feedback } from '@/components/Feedback'
|
import { Feedback } from '@/components/Feedback'
|
||||||
import { Menu } from '@/primitives/Menu'
|
import { Menu } from '@/primitives/Menu'
|
||||||
import { MenuList } from '@/primitives/MenuList'
|
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 = () => {
|
export const Header = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -63,7 +66,7 @@ export const Header = () => {
|
|||||||
<nav>
|
<nav>
|
||||||
<Stack gap={1} direction="row" align="center">
|
<Stack gap={1} direction="row" align="center">
|
||||||
{isLoggedIn === false && !isHome && (
|
{isLoggedIn === false && !isHome && (
|
||||||
<A href={authUrl()}>{t('login')}</A>
|
<ProConnectButton hint={false} />
|
||||||
)}
|
)}
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<Menu>
|
<Menu>
|
||||||
@@ -79,6 +82,8 @@ export const Header = () => {
|
|||||||
items={[{ value: 'logout', label: t('logout') }]}
|
items={[{ value: 'logout', label: t('logout') }]}
|
||||||
onAction={(value) => {
|
onAction={(value) => {
|
||||||
if (value === 'logout') {
|
if (value === 'logout') {
|
||||||
|
terminateAnalyticsSession()
|
||||||
|
terminateSupportSession()
|
||||||
window.location.href = logoutUrl()
|
window.location.href = logoutUrl()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "",
|
"backToHome": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"closeDialog": "",
|
"closeDialog": "",
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "",
|
"loading": "",
|
||||||
"loggedInUserTooltip": "",
|
"loggedInUserTooltip": "",
|
||||||
"login": "Anmelden",
|
"login": {
|
||||||
|
"buttonLabel": "",
|
||||||
|
"linkLabel": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
"logout": "",
|
"logout": "",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": ""
|
"heading": ""
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
"heading": ""
|
"heading": ""
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "",
|
"videoinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
"heading": "",
|
"heading": "",
|
||||||
"joinLabel": "",
|
"joinLabel": "",
|
||||||
"micLabel": "",
|
"joinMeeting": "",
|
||||||
"userLabel": ""
|
"toggleOff": "",
|
||||||
|
"toggleOn": "",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "",
|
||||||
|
"usernameLabel": ""
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "",
|
"leaveRoomPrompt": "",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -37,6 +53,10 @@
|
|||||||
"raise": "",
|
"raise": "",
|
||||||
"lower": ""
|
"lower": ""
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "",
|
||||||
|
"stop": ""
|
||||||
|
},
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "",
|
"open": "",
|
||||||
@@ -49,13 +69,35 @@
|
|||||||
"feedbacks": "",
|
"feedbacks": "",
|
||||||
"support": "",
|
"support": "",
|
||||||
"settings": "",
|
"settings": "",
|
||||||
"username": ""
|
"username": "",
|
||||||
|
"effects": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"participants": {
|
"effects": {
|
||||||
|
"activateCamera": "",
|
||||||
|
"notAvailable": "",
|
||||||
"heading": "",
|
"heading": "",
|
||||||
|
"blur": {
|
||||||
|
"light": "",
|
||||||
|
"normal": "",
|
||||||
|
"apply": "",
|
||||||
|
"clear": ""
|
||||||
|
},
|
||||||
|
"experimental": ""
|
||||||
|
},
|
||||||
|
"sidePanel": {
|
||||||
|
"heading": {
|
||||||
|
"participants": "",
|
||||||
|
"effects": ""
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"participants": "",
|
||||||
|
"effects": ""
|
||||||
|
},
|
||||||
|
"closeButton": ""
|
||||||
|
},
|
||||||
|
"participants": {
|
||||||
"subheading": "",
|
"subheading": "",
|
||||||
"closeButton": "",
|
|
||||||
"contributors": "",
|
"contributors": "",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
"open": "",
|
"open": "",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "Back to homescreen",
|
"backToHome": "Back to homescreen",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"closeDialog": "Close dialog",
|
"closeDialog": "Close dialog",
|
||||||
"error": {
|
"error": {
|
||||||
"heading": "An error occured while loading the page"
|
"heading": "An error occurred while loading the page"
|
||||||
},
|
},
|
||||||
"feedbackAlert": "Give us feedback",
|
"feedbackAlert": "Give us feedback",
|
||||||
"forbidden": {
|
"forbidden": {
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"loggedInUserTooltip": "Logged in as…",
|
"loggedInUserTooltip": "Logged in as…",
|
||||||
"login": "Login",
|
"login": {
|
||||||
|
"buttonLabel": "Login with ProConnect",
|
||||||
|
"linkLabel": "What is ProConnect? - new tab",
|
||||||
|
"link": "What is ProConnect?"
|
||||||
|
},
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": "Page not found"
|
"heading": "Page not found"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"createMeeting": "Create a meeting",
|
"createMeeting": "Create a meeting",
|
||||||
"heading": "Welcome in Meet",
|
"heading": "Welcome in Visio",
|
||||||
"intro": "Work easily, from anywhere.",
|
"intro": "Work easily, from anywhere.",
|
||||||
"joinInputError": "Use a meeting link or code. Examples:",
|
"joinInputError": "Use a meeting link or code. Examples:",
|
||||||
"joinInputExample": "URL or 10-letter code",
|
"joinInputExample": "URL or 10-letter code",
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
{
|
{
|
||||||
"feedback": {
|
"feedback": {
|
||||||
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
|
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
|
||||||
"heading": "Help us improve Meet"
|
"heading": "Help us improve Visio"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Camera",
|
"videoinput": {
|
||||||
"heading": "Join the meeting",
|
"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",
|
||||||
"joinLabel": "Join",
|
"joinLabel": "Join",
|
||||||
"micLabel": "Microphone",
|
"joinMeeting": "Join meeting",
|
||||||
"userLabel": "Your name"
|
"toggleOff": "Click to turn off",
|
||||||
|
"toggleOn": "Click to turn on",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Shown to other participants",
|
||||||
|
"usernameLabel": "Your name"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -27,8 +43,6 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"shareScreen": "Share screen",
|
|
||||||
"stopScreenShare": "Stop screen share",
|
|
||||||
"chat": {
|
"chat": {
|
||||||
"open": "Close the chat",
|
"open": "Close the chat",
|
||||||
"closed": "Open the chat"
|
"closed": "Open the chat"
|
||||||
@@ -37,6 +51,10 @@
|
|||||||
"raise": "Raise hand",
|
"raise": "Raise hand",
|
||||||
"lower": "Lower hand"
|
"lower": "Lower hand"
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "Share screen",
|
||||||
|
"stop": "Stop screen share"
|
||||||
|
},
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "Hide everyone",
|
"open": "Hide everyone",
|
||||||
@@ -49,13 +67,35 @@
|
|||||||
"feedbacks": "Give us feedbacks",
|
"feedbacks": "Give us feedbacks",
|
||||||
"support": "Get Help on Tchap",
|
"support": "Get Help on Tchap",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"username": "Update Your Name"
|
"username": "Update Your Name",
|
||||||
|
"effects": "Apply effects"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"participants": {
|
||||||
"heading": "Participants",
|
|
||||||
"subheading": "In room",
|
"subheading": "In room",
|
||||||
"closeButton": "Hide participants",
|
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"contributors": "Contributors",
|
"contributors": "Contributors",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
|
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
|
||||||
"heading": "Account",
|
"heading": "Account",
|
||||||
"youAreNotLoggedIn": "You are not logged in.",
|
"youAreNotLoggedIn": "You are not logged in.",
|
||||||
"nameLabel": "Votre Nom",
|
"nameLabel": "Your Name",
|
||||||
"authentication": "Authentication"
|
"authentication": "Authentication"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "Retour à l'accueil",
|
"backToHome": "Retour à l'accueil",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"closeDialog": "Fermer la fenêtre modale",
|
"closeDialog": "Fermer la fenêtre modale",
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "Chargement…",
|
"loading": "Chargement…",
|
||||||
"loggedInUserTooltip": "Connecté en tant que…",
|
"loggedInUserTooltip": "Connecté en tant que…",
|
||||||
"login": "Se connecter",
|
"login": {
|
||||||
|
"buttonLabel": "S'identifier avec ProConnect",
|
||||||
|
"linkLabel": "Qu'est-ce que ProConnect ? - nouvelle fenêtre",
|
||||||
|
"link": "Qu'est-ce que ProConnect ?"
|
||||||
|
},
|
||||||
"logout": "Se déconnecter",
|
"logout": "Se déconnecter",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": "Page introuvable"
|
"heading": "Page introuvable"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"createMeeting": "Créer une réunion",
|
"createMeeting": "Créer une réunion",
|
||||||
"heading": "Meet",
|
"heading": "Visio",
|
||||||
"intro": "Collaborez en toute simplicité, où que vous soyez.",
|
"intro": "Collaborez en toute simplicité, où que vous soyez.",
|
||||||
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
||||||
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
|
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
{
|
{
|
||||||
"feedback": {
|
"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.",
|
"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 Meet"
|
"heading": "Aidez-nous à améliorer Visio"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Webcam",
|
"videoinput": {
|
||||||
"heading": "Rejoindre la réunion",
|
"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",
|
||||||
"joinLabel": "Rejoindre",
|
"joinLabel": "Rejoindre",
|
||||||
"micLabel": "Micro",
|
"joinMeeting": "Rejoindre la réjoindre",
|
||||||
"userLabel": "Votre nom"
|
"toggleOff": "Cliquez pour désactiver",
|
||||||
|
"toggleOn": "Cliquez pour activer",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Affiché aux autres participants",
|
||||||
|
"usernameLabel": "Votre nom"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -27,8 +43,6 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"shareScreen": "Partager l'écran",
|
|
||||||
"stopScreenShare": "Arrêter le partage",
|
|
||||||
"chat": {
|
"chat": {
|
||||||
"open": "Masquer le chat",
|
"open": "Masquer le chat",
|
||||||
"closed": "Afficher le chat"
|
"closed": "Afficher le chat"
|
||||||
@@ -37,6 +51,10 @@
|
|||||||
"raise": "Lever la main",
|
"raise": "Lever la main",
|
||||||
"lower": "Baisser la main"
|
"lower": "Baisser la main"
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "Partager l'écran",
|
||||||
|
"stop": "Arrêter le partage"
|
||||||
|
},
|
||||||
"leave": "Quitter",
|
"leave": "Quitter",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "Masquer les participants",
|
"open": "Masquer les participants",
|
||||||
@@ -49,13 +67,35 @@
|
|||||||
"feedbacks": "Partager votre avis",
|
"feedbacks": "Partager votre avis",
|
||||||
"support": "Obtenir de l'aide sur Tchap",
|
"support": "Obtenir de l'aide sur Tchap",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"username": "Choisir votre nom"
|
"username": "Choisir votre nom",
|
||||||
|
"effects": "Appliquer des effets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"participants": {
|
||||||
"heading": "Participants",
|
|
||||||
"subheading": "Dans la réunion",
|
"subheading": "Dans la réunion",
|
||||||
"closeButton": "Masquer les participants",
|
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
"contributors": "Contributeurs",
|
"contributors": "Contributeurs",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
|
|||||||
@@ -134,10 +134,11 @@ export const Checkbox = ({
|
|||||||
>
|
>
|
||||||
<StyledCheckbox {...props}>
|
<StyledCheckbox {...props}>
|
||||||
{(renderProps) => {
|
{(renderProps) => {
|
||||||
renderProps.isInvalid && !!props.validate
|
if (renderProps.isInvalid && !!props.validate) {
|
||||||
? setError(props.validate(renderProps.isSelected))
|
setError(props.validate(renderProps.isSelected))
|
||||||
: setError(null)
|
} else {
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-Checkbox-checkbox">
|
<div className="mt-Checkbox-checkbox">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
|
|||||||
const label = typeof item === 'string' ? item : item.label
|
const label = typeof item === 'string' ? item : item.label
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuItemRecipe()}
|
className={menuItemRecipe({ extraPadding: true })}
|
||||||
key={value}
|
key={value}
|
||||||
id={value as string}
|
id={value as string}
|
||||||
onAction={() => {
|
onAction={() => {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -2,8 +2,10 @@ import { proxy } from 'valtio'
|
|||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showHeader: boolean
|
showHeader: boolean
|
||||||
|
sidePanel: 'participants' | 'effects' | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layoutStore = proxy<State>({
|
export const layoutStore = proxy<State>({
|
||||||
showHeader: false,
|
showHeader: false,
|
||||||
|
sidePanel: null,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { proxy } from 'valtio'
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
showParticipants: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const participantsStore = proxy<State>({
|
|
||||||
showParticipants: false,
|
|
||||||
})
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-backend
|
repository: lasuite/meet-backend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.6"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
migrateJobAnnotations:
|
migrateJobAnnotations:
|
||||||
@@ -108,7 +108,7 @@ frontend:
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-frontend
|
repository: lasuite/meet-frontend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.6"
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-backend
|
repository: lasuite/meet-backend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.6"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
migrateJobAnnotations:
|
migrateJobAnnotations:
|
||||||
@@ -109,7 +109,7 @@ frontend:
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-frontend
|
repository: lasuite/meet-frontend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.6"
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ backend:
|
|||||||
argocd.argoproj.io/hook: PreSync
|
argocd.argoproj.io/hook: PreSync
|
||||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||||
envVars:
|
envVars:
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
|
DJANGO_CSRF_TRUSTED_ORIGINS: http://visio-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
|
||||||
DJANGO_CONFIGURATION: Production
|
DJANGO_CONFIGURATION: Production
|
||||||
DJANGO_ALLOWED_HOSTS: meet-staging.beta.numerique.gouv.fr
|
DJANGO_ALLOWED_HOSTS: visio-staging.beta.numerique.gouv.fr
|
||||||
DJANGO_SECRET_KEY:
|
DJANGO_SECRET_KEY:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: backend
|
name: backend
|
||||||
@@ -42,11 +42,11 @@ backend:
|
|||||||
key: OIDC_RP_CLIENT_SECRET
|
key: OIDC_RP_CLIENT_SECRET
|
||||||
OIDC_RP_SIGN_ALGO: RS256
|
OIDC_RP_SIGN_ALGO: RS256
|
||||||
OIDC_RP_SCOPES: "openid email"
|
OIDC_RP_SCOPES: "openid email"
|
||||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-staging.beta.numerique.gouv.fr
|
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio-staging.beta.numerique.gouv.fr
|
||||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||||
LOGIN_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
|
LOGIN_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
|
||||||
LOGIN_REDIRECT_URL_FAILURE: https://meet-staging.beta.numerique.gouv.fr
|
LOGIN_REDIRECT_URL_FAILURE: https://visio-staging.beta.numerique.gouv.fr
|
||||||
LOGOUT_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
|
LOGOUT_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
|
||||||
DB_HOST:
|
DB_HOST:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: postgresql.postgres.libre.sh
|
name: postgresql.postgres.libre.sh
|
||||||
@@ -112,16 +112,28 @@ frontend:
|
|||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: meet-staging.beta.numerique.gouv.fr
|
host: visio-staging.beta.numerique.gouv.fr
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
additional:
|
||||||
|
- secretName: transitional-tls
|
||||||
|
hosts:
|
||||||
|
- {{ .Values.newDomain }}
|
||||||
|
|
||||||
ingressAdmin:
|
ingressAdmin:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: meet-staging.beta.numerique.gouv.fr
|
host: visio-staging.beta.numerique.gouv.fr
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
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-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
|
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
additional:
|
||||||
|
- secretName: transitional-tls
|
||||||
|
hosts:
|
||||||
|
- {{ .Values.newDomain }}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{{ if .Values.addRedirect }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
{{ if .Values.enablePermanentRedirect }}
|
||||||
|
nginx.ingress.kubernetes.io/permanent-redirect: "https://{{ .Values.newDomain }}$request_uri"
|
||||||
|
nginx.ingress.kubernetes.io/permanent-redirect-code: "308"
|
||||||
|
{{ end }}
|
||||||
|
name: temporary-redirect
|
||||||
|
namespace: {{ .Release.Namespace | quote }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.oldDomain }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: meet-frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: meet-backend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.oldDomain }}
|
||||||
|
secretName: transitional-tls
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: transitional-tls
|
||||||
|
namespace: meet-staging
|
||||||
|
spec:
|
||||||
|
dnsNames:
|
||||||
|
- {{ .Values.newDomain }}
|
||||||
|
- {{ .Values.oldDomain }}
|
||||||
|
issuerRef:
|
||||||
|
group: cert-manager.io
|
||||||
|
kind: ClusterIssuer
|
||||||
|
name: letsencrypt-prod
|
||||||
|
secretName: transitional-tls
|
||||||
|
usages:
|
||||||
|
- digital signature
|
||||||
|
- key encipherment
|
||||||
|
{{ end }}
|
||||||
@@ -36,6 +36,13 @@ releases:
|
|||||||
chart: ./extra
|
chart: ./extra
|
||||||
secrets:
|
secrets:
|
||||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||||
|
values:
|
||||||
|
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||||
|
- addRedirect: {{ .Values | get "addRedirect" "False" }}
|
||||||
|
enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
||||||
|
oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
||||||
|
tlsOldSecretName: {{ .Values | get "tlsOldSecretName" "tls"}}
|
||||||
|
newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
||||||
|
|
||||||
- name: meet
|
- name: meet
|
||||||
version: {{ .Values.version }}
|
version: {{ .Values.version }}
|
||||||
@@ -64,6 +71,11 @@ environments:
|
|||||||
staging:
|
staging:
|
||||||
values:
|
values:
|
||||||
- version: 0.0.1
|
- version: 0.0.1
|
||||||
|
addRedirect: True
|
||||||
|
enablePermanentRedirect: True
|
||||||
|
oldDomain: meet-staging.beta.numerique.gouv.fr
|
||||||
|
tlsOldSecretName: meet-tls
|
||||||
|
newDomain: visio-staging.beta.numerique.gouv.fr
|
||||||
secrets:
|
secrets:
|
||||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||||
preprod:
|
preprod:
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ kind: Secret
|
|||||||
metadata:
|
metadata:
|
||||||
name: backend
|
name: backend
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
namespace: {{ .Release.Namespace | quote }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": pre-install,pre-upgrade
|
||||||
|
"helm.sh/hook-weight": "-5"
|
||||||
|
"helm.sh/hook-delete-policy": before-hook-creation
|
||||||
stringData:
|
stringData:
|
||||||
DJANGO_SUPERUSER_EMAIL: {{ .Values.djangoSuperUserEmail }}
|
DJANGO_SUPERUSER_EMAIL: {{ .Values.djangoSuperUserEmail }}
|
||||||
DJANGO_SUPERUSER_PASSWORD: {{ .Values.djangoSuperUserPass }}
|
DJANGO_SUPERUSER_PASSWORD: {{ .Values.djangoSuperUserPass }}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.5.4",
|
"@html-to/text-cli": "0.5.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "16.15.1"
|
"node": "20.17.0"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/numerique-gouv/meet",
|
"repository": "https://github.com/numerique-gouv/meet",
|
||||||
"author": "DINUM",
|
"author": "DINUM",
|
||||||
|
|||||||
Reference in New Issue
Block a user