Compare commits

..

5 Commits

Author SHA1 Message Date
lebaudantoine dc9dedb91b wip update tag 2024-09-12 11:19:23 +02:00
lebaudantoine 3da3641a19 🚧 (frontend) Add start/stop audio recording menu item
Functional but not mergeable—remote participants need to be informed
when recording starts, including the egress ID to prevent collisions.

Quick and dirty implementation.
2024-09-12 10:58:21 +02:00
lebaudantoine 21626b4c4c 🚧(frontend) show recording status indicator
Room recording status triggers only when the egress worker is active.
Querying the Egress API starts the worker, which joins the room as an invisible participant.
The room is marked as "recording" only once the worker has fully joined.
Not mergeable yet.

Sadly the room event gives no information about the Egress worker’s ID.
2024-09-12 10:58:19 +02:00
lebaudantoine f16cedf5a5 🚧(frontend) draft audio-only room recording method
(roomId is wrongly named, and designates room’s slug)

Room’s slug are passed in filenames to simplify post-processing. Thus, from filename,
we can retrieve the room being recorded.

Current limitation: audio tracks are mixed, requiring diarization.
Future plans include full audio + video recording for a more complete meeting experience.
2024-09-12 10:49:00 +02:00
lebaudantoine 00e4679dc5 🚧(backend) allow room recording
Added 'room_record' grant to LiveKit token to enable room recording.
Initial implementation allows any authenticated user to record,
but this needs refinement.

Only admins and persisted room members should have this capability,
as they are the ones notified post-recording. Not ready for merge.
2024-09-12 10:49:00 +02:00
67 changed files with 3192 additions and 3517 deletions
+1 -3
View File
@@ -13,9 +13,7 @@
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"eslint"
]
"matchPackageNames": []
}
]
}
+1
View File
@@ -53,6 +53,7 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str:
room=room,
room_join=True,
room_admin=True,
room_record=True,
can_update_own_metadata=True,
can_publish_sources=[
"camera",
-1
View File
@@ -258,7 +258,6 @@ class Base(Configuration):
EMAIL_HOST_PASSWORD = values.Value(None)
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
AUTH_USER_MODEL = "core.User"
+10 -10
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.6"
version = "0.1.5"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.19",
"boto3==1.35.10",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -36,11 +36,11 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.1.1",
"django==5.1",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"easy_thumbnails==2.9",
"factory_boy==3.3.1",
"freezegun==1.5.1",
"gunicorn==23.0.0",
@@ -48,11 +48,11 @@ dependencies = [
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.2",
"psycopg[binary]==3.2.1",
"PyJWT==2.9.0",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.14.0",
"sentry-sdk==2.13.0",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.7.0",
@@ -76,13 +76,13 @@ dev = [
"pylint-django==2.5.5",
"pylint==3.2.7",
"pytest-cov==5.0.0",
"pytest-django==4.9.0",
"pytest==8.3.3",
"pytest-django==4.8.0",
"pytest==8.3.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.5",
"types-requests==2.32.0.20240914",
"ruff==0.6.3",
"types-requests==2.32.0.20240712",
]
[tool.setuptools]
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Visio</title>
<title>Meet</title>
</head>
<body>
<div id="root"></div>
+2608 -2319
View File
File diff suppressed because it is too large Load Diff
+27 -30
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,48 +13,45 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.5.4",
"@livekit/components-styles": "1.1.2",
"@livekit/track-processors": "0.3.2",
"@pandacss/preset-panda": "0.46.0",
"@livekit/components-react": "2.3.3",
"@livekit/components-styles": "1.0.12",
"@pandacss/preset-panda": "0.41.0",
"@react-aria/toast": "3.0.0-beta.15",
"@remixicon/react": "4.2.0",
"@tanstack/react-query": "5.56.2",
"crisp-sdk-web": "1.0.25",
"@tanstack/react-query": "5.49.2",
"hoofd": "1.7.1",
"i18next": "23.15.1",
"i18next": "23.12.1",
"i18next-browser-languagedetector": "8.0.0",
"i18next-parser": "9.0.2",
"i18next-parser": "9.0.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.5.1",
"posthog-js": "1.161.5",
"react": "18.3.1",
"react-aria-components": "1.3.3",
"react-dom": "18.3.1",
"react-i18next": "15.0.2",
"livekit-client": "2.3.1",
"react": "18.2.0",
"react-aria-components": "1.2.1",
"react-dom": "18.2.0",
"react-i18next": "14.1.3",
"use-sound": "4.0.3",
"valtio": "2.0.0",
"wouter": "3.3.5"
"valtio": "1.13.2",
"wouter": "3.3.0"
},
"devDependencies": {
"@pandacss/dev": "0.46.0",
"@tanstack/eslint-plugin-query": "5.56.1",
"@tanstack/react-query-devtools": "5.56.2",
"@types/node": "20.16.5",
"@types/react": "18.3.5",
"@pandacss/dev": "0.41.0",
"@tanstack/eslint-plugin-query": "5.49.1",
"@tanstack/react-query-devtools": "5.49.2",
"@types/node": "20.14.9",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "8.5.0",
"@typescript-eslint/parser": "8.5.0",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"@vitejs/plugin-react": "4.3.1",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.12",
"postcss": "8.4.47",
"eslint-plugin-react-refresh": "0.4.7",
"postcss": "8.4.39",
"prettier": "3.3.3",
"typescript": "5.6.2",
"vite": "5.4.5",
"vite-tsconfig-paths": "5.0.1"
"typescript": "5.5.2",
"vite": "5.3.1",
"vite-tsconfig-paths": "4.3.2"
}
}
-5
View File
@@ -13,8 +13,6 @@ import { routes } from './routes'
import './i18n/init'
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
import { queryClient } from '@/api/queryClient'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
function App() {
const { i18n } = useTranslation()
@@ -23,9 +21,6 @@ function App() {
const isProduction = import.meta.env.PROD
silenceLiveKitLogs(isProduction)
useAnalytics()
useSupport()
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={null}>
File diff suppressed because one or more lines are too long
@@ -1,42 +0,0 @@
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
}
+1 -11
View File
@@ -2,12 +2,9 @@ import { useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import { fetchUser } from './fetchUser'
import { type ApiUser } from './ApiUser'
import { useEffect } from 'react'
import { startAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { initializeSupportSession } from '@/features/support/hooks/useSupport'
/**
* returns info about currently logged-in user
* returns info about currently logged in user
*
* `isLoggedIn` is undefined while query is loading and true/false when it's done
*/
@@ -18,13 +15,6 @@ export const useUser = () => {
staleTime: 1000 * 60 * 60, // 1 hour
})
useEffect(() => {
if (query?.data) {
startAnalyticsSession(query.data)
initializeSupportSession(query.data)
}
}, [query.data])
const isLoggedIn =
query.status === 'success' ? query.data !== false : undefined
const isLoggedOut = isLoggedIn === false
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
name="roomId"
label={t('joinInputLabel')}
description={t('joinInputExample', {
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
})}
validate={(value) => {
return !isRoomValid(value.trim()) ? (
@@ -44,7 +44,6 @@ export const LaterMeetingDialog = ({
setIsCopied(true)
}}
onHoverChange={setIsHovered}
data-attr="later-dialog-copy"
>
{isCopied ? (
<>
+9 -17
View File
@@ -1,18 +1,18 @@
import { useTranslation } from 'react-i18next'
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
import { DialogTrigger } from 'react-aria-components'
import { Button, Menu, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { Centered } from '@/layout/Centered'
import { generateRoomId } from '@/features/rooms'
import { useUser, UserAware } from '@/features/auth'
import { authUrl, useUser, UserAware } from '@/features/auth'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
import { ProConnectButton } from '@/components/ProConnectButton'
import { useCreateRoom } from '@/features/rooms'
import { usePersistentUserChoices } from '@livekit/components-react'
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import { RiAddLine, RiLink } from '@remixicon/react'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { useState } from 'react'
@@ -42,12 +42,10 @@ export const Home = () => {
{t('loginToCreateMeeting')}
</Text>
)}
<HStack gap="gutter" alignItems="start">
<HStack gap="gutter">
{isLoggedIn ? (
<Menu>
<Button variant="primary" data-attr="create-meeting">
{t('createMeeting')}
</Button>
<Button variant="primary">{t('createMeeting')}</Button>
<RACMenu>
<MenuItem
className={menuItemRecipe({ icon: true })}
@@ -59,7 +57,6 @@ export const Home = () => {
})
)
}}
data-attr="create-option-instant"
>
<RiAddLine size={18} />
{t('createMenu.instantOption')}
@@ -72,7 +69,6 @@ export const Home = () => {
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
>
<RiLink size={18} />
{t('createMenu.laterOption')}
@@ -80,16 +76,12 @@ export const Home = () => {
</RACMenu>
</Menu>
) : (
<ProConnectButton />
<Button variant="primary" href={authUrl()}>
{t('login', { ns: 'global' })}
</Button>
)}
<DialogTrigger>
<Button
variant="primary"
outline
style={{
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
}}
>
<Button variant="primary" outline>
{t('joinMeeting')}
</Button>
<JoinMeetingDialog />
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { Participant, RoomEvent } from 'livekit-client'
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
@@ -50,32 +50,31 @@ export const MainNotificationToast = () => {
}
}, [room])
// fixme - close all related toasters when hands are lowered remotely
useEffect(() => {
const decoder = new TextDecoder()
const handleNotificationReceived = (
prevMetadataStr: string | undefined,
participant: Participant
payload: Uint8Array,
participant?: RemoteParticipant
) => {
if (!participant) return
if (isMobileBrowser()) return
if (participant.isLocal) return
const prevMetadata = JSON.parse(prevMetadataStr || '{}')
const metadata = JSON.parse(participant.metadata || '{}')
if (prevMetadata.raised == metadata.raised) return
if (!participant) {
return
}
if (isMobileBrowser()) {
return
}
const notification = decoder.decode(payload)
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Raised
)
if (existingToast && prevMetadata.raised && !metadata.raised) {
if (existingToast && notification === NotificationType.Lowered) {
toastQueue.close(existingToast.key)
return
}
if (!existingToast && !prevMetadata.raised && metadata.raised) {
if (!existingToast && notification === NotificationType.Raised) {
triggerNotificationSound(NotificationType.Raised)
toastQueue.add(
{
@@ -87,23 +86,13 @@ export const MainNotificationToast = () => {
}
}
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
room.on(RoomEvent.DataReceived, handleNotificationReceived)
return () => {
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
room.off(RoomEvent.DataReceived, handleNotificationReceived)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const closeAllToasts = () => {
toastQueue.visibleToasts.forEach(({ key }) => toastQueue.close(key))
}
room.on(RoomEvent.Disconnected, closeAllToasts)
return () => {
room.off(RoomEvent.Disconnected, closeAllToasts)
}
}, [room])
return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
<ToastProvider />
@@ -9,6 +9,7 @@ import {
import { Room, RoomOptions } from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { QueryAware } from '@/components/QueryAware'
import { ErrorScreen } from '@/components/ErrorScreen'
@@ -18,7 +19,6 @@ import { useCreateRoom } from '../api/createRoom'
import { InviteDialog } from './InviteDialog'
import { VideoConference } from '../livekit/prefabs/VideoConference'
import posthog from 'posthog-js'
export const Conference = ({
roomId,
@@ -31,9 +31,6 @@ export const Conference = ({
mode?: 'join' | 'create'
initialRoomData?: ApiRoom
}) => {
useEffect(() => {
posthog.capture('visit-room', { slug: roomId })
}, [roomId])
const fetchKey = [keys.room, roomId, userConfig.username]
const {
@@ -82,6 +79,25 @@ export const Conference = ({
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
/**
* checks for actual click on the leave button instead of
* relying on LiveKitRoom onDisconnected because onDisconnected
* triggers even on page reload, it's not a user "onLeave" event really.
* Here we want to react to the user actually deciding to leave.
*/
useEffect(() => {
const checkOnLeaveClick = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.classList.contains('lk-disconnect-button')) {
navigateTo('feedback')
}
}
document.body.addEventListener('click', checkOnLeaveClick)
return () => {
document.body.removeEventListener('click', checkOnLeaveClick)
}
}, [])
const { t } = useTranslation('rooms')
if (isCreateError) {
// this error screen should be replaced by a proper waiting room for anonymous user.
@@ -90,7 +90,6 @@ export const InviteDialog = ({
setIsCopied(true)
}}
onHoverChange={setIsHovered}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
@@ -16,8 +16,8 @@ export const Join = ({
<PreJoin
persistUserChoices
onSubmit={onSubmit}
micLabel={t('join.audioinput.label')}
camLabel={t('join.videoinput.label')}
micLabel={t('join.micLabel')}
camLabel={t('join.camlabel')}
joinLabel={t('join.joinLabel')}
userLabel={t('join.userLabel')}
/>
@@ -11,7 +11,6 @@ export const useLowerHandParticipants = () => {
)
return Promise.all(promises)
} catch (error) {
console.error('An error occurred while lowering hands :', error)
throw new Error('An error occurred while lowering hands.')
}
}
@@ -0,0 +1,56 @@
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
import { useParams } from 'wouter'
export const useRecordRoom = () => {
const data = useRoomData()
const { roomId: roomSlug } = useParams()
const recordRoom = () => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
if (!roomSlug) {
throw new Error('Room ID is not available')
}
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'/twirp/livekit.Egress/StartRoomCompositeEgress'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room_name: data.livekit.room,
audio_only: true,
file_outputs: [
{
file_extension: 'ogg',
filepath: `{room_name}_{time}_${roomSlug}`,
},
],
}),
}
)
}
const stopRecordingRoom = (egressId: string) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
return fetchServerApi(
buildServerApiUrl(data.livekit.url, '/twirp/livekit.Egress/StopEgress'),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
egressId,
}),
}
)
}
return { recordRoom, stopRecordingRoom }
}
@@ -1,191 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { Text, P, ToggleButton, Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import {
BackgroundBlur,
BackgroundOptions,
ProcessorWrapper,
BackgroundTransformer,
} from '@livekit/track-processors'
const Information = styled('div', {
base: {
backgroundColor: 'orange.50',
borderRadius: '4px',
padding: '0.75rem 0.75rem',
marginTop: '0.8rem',
alignItems: 'start',
},
})
enum BlurRadius {
NONE = 0,
LIGHT = 5,
NORMAL = 10,
}
export const Effects = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const { isCameraEnabled, cameraTrack, localParticipant } =
useLocalParticipant()
const videoRef = useRef<HTMLVideoElement>(null)
const [processorPending, setProcessorPending] = useState(false)
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
const getProcessor = () => {
return localCameraTrack?.getProcessor() as ProcessorWrapper<BackgroundOptions>
}
const getBlurRadius = (): BlurRadius => {
const processor = getProcessor()
return (
(processor?.transformer as BackgroundTransformer)?.options?.blurRadius ||
BlurRadius.NONE
)
}
const toggleBlur = async (blurRadius: number) => {
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true)
if (!localCameraTrack) return
setProcessorPending(true)
const processor = getProcessor()
const currentBlurRadius = getBlurRadius()
try {
if (blurRadius == currentBlurRadius && processor) {
await localCameraTrack.stopProcessor()
} else if (!processor) {
await localCameraTrack.setProcessor(BackgroundBlur(blurRadius))
} else {
await processor?.updateTransformerOptions({ blurRadius })
}
} catch (error) {
console.error('Error applying blur:', error)
} finally {
setProcessorPending(false)
}
}
useEffect(() => {
const videoElement = videoRef.current
if (!videoElement) return
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement)
attachVideoTrack()
return () => {
if (!videoElement) return
localCameraTrack.detach(videoElement)
}
}, [localCameraTrack, isCameraEnabled])
const isSelected = (blurRadius: BlurRadius) => {
return isCameraEnabled && getBlurRadius() == blurRadius
}
const tooltipLabel = (blurRadius: BlurRadius) => {
return t(`blur.${isSelected(blurRadius) ? 'clear' : 'apply'}`)
}
return (
<VStack padding="0 1.5rem">
{localCameraTrack && isCameraEnabled ? (
<video
ref={videoRef}
width="100%"
muted
style={{
transform: 'rotateY(180deg)',
minHeight: '175px',
borderRadius: '8px',
}}
/>
) : (
<div
style={{
width: '100%',
height: '174px',
display: 'flex',
backgroundColor: 'black',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<P
style={{
color: 'white',
textAlign: 'center',
textWrap: 'balance',
marginBottom: 0,
}}
>
{t('activateCamera')}
</P>
</div>
)}
<Div
alignItems={'left'}
width={'100%'}
style={{
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
padding: '0.5rem 1rem',
}}
>
<H
lvl={3}
style={{
marginBottom: '0.4rem',
}}
>
{t('heading')}
</H>
{ProcessorWrapper.isSupported ? (
<HStack>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.LIGHT)}
tooltip={tooltipLabel(BlurRadius.LIGHT)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.LIGHT)}
isSelected={isSelected(BlurRadius.LIGHT)}
>
{t('blur.light')}
</ToggleButton>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.NORMAL)}
tooltip={tooltipLabel(BlurRadius.NORMAL)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.NORMAL)}
isSelected={isSelected(BlurRadius.NORMAL)}
>
{t('blur.normal')}
</ToggleButton>
</HStack>
) : (
<Text variant="sm">{t('notAvailable')}</Text>
)}
<Information>
<Text
variant="sm"
style={{
textWrap: 'balance',
}}
>
{t('experimental')}
</Text>
</Information>
</Div>
</VStack>
)
}
@@ -0,0 +1,32 @@
import { useRoomContext } from '@livekit/components-react'
import { useEffect, useState } from 'react'
import { RoomEvent } from 'livekit-client'
export const RecordingIndicator = () => {
const room = useRoomContext()
const [isRecording, setIsRecording] = useState(room.isRecording)
useEffect(() => {
const handleRecordingStatusChanges = (isRecording: boolean) => {
setIsRecording(isRecording)
}
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
return () => {
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
}
}, [room])
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
width: '100%',
}}
>
Room is recording: {isRecording ? 'yes' : 'no'}
</div>
)
}
@@ -1,88 +0,0 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Box, Button, Div } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useWidgetInteraction } from '../hooks/useWidgetInteraction'
import { ReactNode } from 'react'
import { Effects } from './Effects'
type StyledSidePanelProps = {
title: string
children: ReactNode
onClose: () => void
closeButtonTooltip: string
}
const StyledSidePanel = ({
title,
children,
onClose,
closeButtonTooltip,
}: StyledSidePanelProps) => (
<Box
size="sm"
minWidth="360px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0,
gap: 0,
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
>
{title}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">{children}</Div>
</Box>
)
export const SidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const { isParticipantsOpen, isEffectsOpen } = useWidgetInteraction()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
if (!sidePanel) {
return
}
return (
<StyledSidePanel
title={t(`heading.${sidePanel}`)}
onClose={() => (layoutStore.sidePanel = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${sidePanel}`),
})}
>
{isParticipantsOpen && <ParticipantsList />}
{isEffectsOpen && <Effects />}
</StyledSidePanel>
)
}
@@ -5,7 +5,7 @@ import { css } from '@/styled-system/css'
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
export const ChatToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
const { t } = useTranslation('rooms')
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
const tooltipLabel = isChatOpen ? 'open' : 'closed'
@@ -20,11 +20,10 @@ export const ChatToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
aria-label={t(`controls.chat.${tooltipLabel}`)}
tooltip={t(`controls.chat.${tooltipLabel}`)}
isSelected={isChatOpen}
onPress={() => toggleChat()}
data-attr={`controls-chat-${tooltipLabel}`}
>
<RiChat1Line />
</ToggleButton>
@@ -4,16 +4,30 @@ import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { NotificationType } from '@/features/notifications/NotificationType'
export const HandToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
const { t } = useTranslation('rooms')
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
const label = isHandRaised
? 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 (
<div
@@ -25,11 +39,13 @@ export const HandToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
aria-label={label}
tooltip={label}
isSelected={isHandRaised}
onPress={() => toggleRaisedHand()}
data-attr={`controls-hand-${tooltipLabel}`}
onPress={() => {
notifyOtherParticipants(isHandRaised)
toggleRaisedHand()
}}
>
<RiHand />
</ToggleButton>
@@ -1,37 +0,0 @@
import { useConnectionState, useRoomContext } from '@livekit/components-react'
import { Button } from '@/primitives'
import { navigateTo } from '@/navigation/navigateTo'
import { RiPhoneFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ConnectionState } from 'livekit-client'
export const LeaveButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const room = useRoomContext()
const connectionState = useConnectionState(room)
return (
<Button
isDisabled={connectionState === ConnectionState.Disconnected}
variant={'danger'}
tooltip={t('leave')}
aria-label={t('leave')}
onPress={() => {
room
.disconnect(true)
.catch((e) =>
console.error('An error occurred while disconnecting:', e)
)
.finally(() => {
navigateTo('feedback')
})
}}
data-attr="controls-leave"
>
<RiPhoneFill
style={{
transform: 'rotate(135deg)',
}}
/>
</Button>
)
}
@@ -1,16 +1,14 @@
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import {
RiAccountBoxLine,
RiFeedbackLine,
RiQuestionLine,
RiSettings3Line,
} from '@remixicon/react'
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction } from 'react'
import { DialogState } from './OptionsButton'
import { Separator } from '@/primitives/Separator'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
import { RecordingMenuItem } from './RecordingMenuItem.tsx'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
@@ -18,8 +16,8 @@ export const OptionsMenuItems = ({
}: {
onOpenDialog: Dispatch<SetStateAction<DialogState>>
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useWidgetInteraction()
const { t } = useTranslation('rooms')
return (
<RACMenu
style={{
@@ -27,41 +25,30 @@ export const OptionsMenuItems = ({
width: '300px',
}}
>
<Section>
<MenuItem
onAction={() => toggleEffects()}
className={menuItemRecipe({ icon: true })}
>
<RiAccountBoxLine size={20} />
{t('effects')}
</MenuItem>
</Section>
<Separator />
<Section>
<MenuItem
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiQuestionLine size={20} />
{t('support')}
</MenuItem>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={20} />
{t('feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={20} />
{t('settings')}
</MenuItem>
</Section>
<MenuItem
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiQuestionLine size={18} />
{t('options.items.support')}
</MenuItem>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={18} />
{t('options.items.feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={18} />
{t('options.items.settings')}
</MenuItem>
<RecordingMenuItem />
</RACMenu>
)
}
@@ -0,0 +1,51 @@
import { MenuItem } from 'react-aria-components'
import { menuItemRecipe } from '@/primitives/menuItemRecipe.ts'
import { RiPauseCircleLine, RiRecordCircleLine } from '@remixicon/react'
import { useRecordRoom } from '@/features/rooms/livekit/api/recordRoom.ts'
import { useState } from 'react'
import { egressStore } from '@/stores/egress.tsx'
import { useSnapshot } from 'valtio'
export const RecordingMenuItem = () => {
const { recordRoom, stopRecordingRoom } = useRecordRoom()
const egressSnap = useSnapshot(egressStore)
const egressId = egressSnap.egressId
const [isPending, setIsPending] = useState(false)
const handleAction = async () => {
if (egressId) {
setIsPending(true)
const response = await stopRecordingRoom(egressId)
console.log(response)
egressStore.egressId = undefined
setIsPending(false)
} else {
setIsPending(true)
const response = await recordRoom()
egressStore.egressId = response['egress_id'] as string
setIsPending(false)
}
}
return (
<MenuItem
isDisabled={isPending}
className={menuItemRecipe({ icon: true })}
onAction={handleAction}
>
{egressId ? (
<>
<RiPauseCircleLine size={18} />
Stop recording room
</>
) : (
<>
<RiRecordCircleLine size={18} />
Record room
</>
)}
</MenuItem>
)
}
@@ -73,7 +73,6 @@ export const HandRaisedListItem = ({
size="sm"
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
data-attr="participants-lower-hand"
>
<RiHand />
</Button>
@@ -19,7 +19,6 @@ export const LowerAllHandsButton = ({
fullWidth
variant="text"
onPress={() => lowerHandParticipants(participants)}
data-attr="participants-lower-hands"
>
{t('participants.lowerParticipantsHand')}
</Button>
@@ -78,7 +78,6 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
? muteParticipant(participant)
: setIsAlertOpen(true)
}
data-attr="participants-mute"
>
{isMuted ? (
<RiMicOffFill color={'gray'} />
@@ -1,27 +1,27 @@
import { css } from '@/styled-system/css'
import { allParticipantRoomEvents } from '@livekit/components-core'
import { useParticipants } from '@livekit/components-react'
import { Div, H } from '@/primitives'
import { Heading } from 'react-aria-components'
import { Box, Button, Div, H } from '@/primitives'
import { text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
import { participantsStore } from '@/stores/participants'
import { useTranslation } from 'react-i18next'
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
import { RoomEvent } from 'livekit-client'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
const { t } = useTranslation('rooms')
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
// because the 'useLocalParticipant' hook does not update the participant's information when their
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
const participants = useParticipants({
updateOnlyOn: [
RoomEvent.ParticipantNameChanged,
...allParticipantRoomEvents,
],
updateOnlyOn: allParticipantRoomEvents,
})
const sortedRemoteParticipants = participants
@@ -44,40 +44,75 @@ export const ParticipantsList = () => {
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
<>
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
marginBottom: '0.83em',
})}
<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',
}}
>
{t('subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<Div marginBottom=".9375rem">
<ParticipantsCollapsableList
heading={t('raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
action={() => (
<LowerAllHandsButton participants={raisedHandParticipants} />
)}
/>
</Div>
)}
<ParticipantsCollapsableList
heading={t('contributors')}
participants={sortedParticipants}
renderParticipant={(participant) => (
<ParticipantListItem participant={participant} />
{t('participants.heading')}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={() => (participantsStore.showParticipants = false)}
aria-label={t('participants.closeButton')}
tooltip={t('participants.closeButton')}
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
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>
)
}
@@ -6,7 +6,7 @@ import { useParticipants } from '@livekit/components-react'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
export const ParticipantsToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
const { t } = useTranslation('rooms')
/**
* Context could not be used due to inconsistent refresh behavior.
@@ -30,11 +30,10 @@ export const ParticipantsToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
aria-label={t(`controls.participants.${tooltipLabel}`)}
tooltip={t(`controls.participants.${tooltipLabel}`)}
isSelected={isParticipantsOpen}
onPress={() => toggleParticipants()}
data-attr={`controls-participants-${tooltipLabel}`}
>
<RiGroupLine />
</ToggleButton>
@@ -1,56 +0,0 @@
import { Div, ToggleButton } from '@/primitives'
import { RiArrowUpLine, RiCloseFill, RiRectangleLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Track } from 'livekit-client'
import React from 'react'
export const ScreenShareToggle = (
props: Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
'source' | 'captureOptions'
>
) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
const { buttonProps, enabled } = useTrackToggle({
...props,
source: Track.Source.ScreenShare,
captureOptions: { audio: true, selfBrowserSurface: 'include' },
})
const tooltipLabel = enabled ? 'stop' : 'start'
const Icon = enabled ? RiCloseFill : RiArrowUpLine
// fixme - remove ToggleButton custom styles when we design a proper icon
return (
<ToggleButton
isSelected={enabled}
square
legacyStyle
tooltip={t(tooltipLabel)}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
)
}
style={{
maxWidth: '46px',
maxHeight: '46px',
}}
data-attr={`controls-screenshare-${tooltipLabel}`}
>
<Div position="relative">
<RiRectangleLine size={28} />
<Icon
size={16}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
</Div>
</ToggleButton>
)
}
@@ -1,120 +0,0 @@
import { useTranslation } from 'react-i18next'
import {
useMediaDeviceSelect,
useTrackToggle,
UseTrackToggleProps,
} from '@livekit/components-react'
import { HStack } from '@/styled-system/jsx'
import { Button, Menu, MenuList, ToggleButton } from '@/primitives'
import {
RemixiconComponentType,
RiArrowDownSLine,
RiMicLine,
RiMicOffLine,
RiVideoOffLine,
RiVideoOnLine,
} from '@remixicon/react'
import { Track } from 'livekit-client'
import React from 'react'
export type ToggleSource = Exclude<
Track.Source,
Track.Source.ScreenShareAudio | Track.Source.Unknown
>
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
type SelectToggleDeviceConfig = {
kind: MediaDeviceKind
iconOn: RemixiconComponentType
iconOff: RemixiconComponentType
}
type SelectToggleDeviceConfigMap = {
[key in SelectToggleSource]: SelectToggleDeviceConfig
}
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
[Track.Source.Microphone]: {
kind: 'audioinput',
iconOn: RiMicLine,
iconOff: RiMicOffLine,
},
[Track.Source.Camera]: {
kind: 'videoinput',
iconOn: RiVideoOnLine,
iconOff: RiVideoOffLine,
},
}
type SelectToggleDeviceProps<T extends ToggleSource> =
UseTrackToggleProps<T> & {
onActiveDeviceChange: (deviceId: string) => void
source: SelectToggleSource
}
export const SelectToggleDevice = <T extends ToggleSource>({
onActiveDeviceChange,
...props
}: SelectToggleDeviceProps<T>) => {
const config = selectToggleDeviceConfig[props.source]
if (!config) {
throw new Error('Invalid source')
}
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { buttonProps, enabled } = useTrackToggle(props)
const { kind, iconOn, iconOff } = config
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind })
const toggleLabel = t(enabled ? 'disable' : 'enable', {
keyPrefix: `join.${kind}`,
})
const selectLabel = t('choose', { keyPrefix: `join.${kind}` })
const Icon = enabled ? iconOn : iconOff
return (
<HStack gap={0}>
<ToggleButton
isSelected={enabled}
variant={enabled ? undefined : 'danger'}
toggledStyles={false}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement>
)
}
aria-label={toggleLabel}
tooltip={toggleLabel}
groupPosition="left"
>
<Icon />
</ToggleButton>
<Menu>
<Button
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
>
<RiArrowDownSLine />
</Button>
<MenuList
items={devices.map((d) => ({
value: d.deviceId,
label: d.label,
}))}
selectedItem={activeDeviceId}
onAction={(value) => {
setActiveMediaDevice(value as string)
onActiveDeviceChange(value as string)
}}
/>
</Menu>
</HStack>
)
}
@@ -0,0 +1,33 @@
import { RoomEvent } from 'livekit-client'
// Issue: 'allRemoteParticipantRoomEvents' is not exposed or importable. One event is missing
// to trigger the real-time update of participants when they change their name.
// This code is duplicated from LiveKit.
export const allRemoteParticipantRoomEvents = [
RoomEvent.ConnectionStateChanged,
RoomEvent.RoomMetadataChanged,
RoomEvent.ActiveSpeakersChanged,
RoomEvent.ConnectionQualityChanged,
RoomEvent.ParticipantConnected,
RoomEvent.ParticipantDisconnected,
RoomEvent.ParticipantPermissionsChanged,
RoomEvent.ParticipantMetadataChanged,
RoomEvent.ParticipantNameChanged, // This element is missing in LiveKit and causes problems
RoomEvent.TrackMuted,
RoomEvent.TrackUnmuted,
RoomEvent.TrackPublished,
RoomEvent.TrackUnpublished,
RoomEvent.TrackStreamStateChanged,
RoomEvent.TrackSubscriptionFailed,
RoomEvent.TrackSubscriptionPermissionChanged,
RoomEvent.TrackSubscriptionStatusChanged,
]
export const allParticipantRoomEvents = [
...allRemoteParticipantRoomEvents,
RoomEvent.LocalTrackPublished,
RoomEvent.LocalTrackUnpublished,
]
@@ -7,7 +7,6 @@ type useRaisedHandProps = {
}
export function useRaisedHand({ participant }: useRaisedHandProps) {
// fixme - refactor this part to rely on attributes
const { metadata } = useParticipantInfo({ participant })
const parsedMetadata = JSON.parse(metadata || '{}')
@@ -1,46 +1,34 @@
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { participantsStore } from '@/stores/participants.ts'
export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == 'participants'
const isEffectsOpen = sidePanel == 'effects'
const participantsSnap = useSnapshot(participantsStore)
const isParticipantsOpen = participantsSnap.showParticipants
const toggleParticipants = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
participantsStore.showParticipants = !isParticipantsOpen
}
const toggleChat = () => {
if (isParticipantsOpen) {
layoutStore.sidePanel = null
participantsStore.showParticipants = false
}
if (dispatch) {
dispatch({ msg: 'toggle_chat' })
}
}
const toggleEffects = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isEffectsOpen ? null : 'effects'
}
return {
toggleParticipants,
toggleChat,
toggleEffects,
isChatOpen: state?.showChat,
unreadMessages: state?.unreadMessages,
isParticipantsOpen,
isEffectsOpen,
}
}
@@ -4,6 +4,10 @@ import * as React from 'react'
import { supportsScreenSharing } from '@livekit/components-core'
import {
DisconnectButton,
LeaveIcon,
MediaDeviceMenu,
TrackToggle,
useMaybeLayoutContext,
usePersistentUserChoices,
} from '@livekit/components-react'
@@ -11,13 +15,11 @@ import {
import { mergeProps } from '@/utils/mergeProps.ts'
import { StartMediaButton } from '../components/controls/StartMediaButton'
import { useMediaQuery } from '../hooks/useMediaQuery'
import { useTranslation } from 'react-i18next'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '../components/controls/ChatToggle'
import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
/** @public */
export type ControlBarControls = {
@@ -36,7 +38,7 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
controls?: ControlBarControls
/**
* If `true`, the user's device choices will be persisted.
* This will enable the user to have the same device choices when they rejoin the room.
* This will enables the user to have the same device choices when they rejoin the room.
* @defaultValue true
* @alpha
*/
@@ -65,6 +67,7 @@ export function ControlBar({
onDeviceError,
...props
}: ControlBarProps) {
const { t } = useTranslation('rooms')
const [isChatOpen, setIsChatOpen] = React.useState(false)
const layoutContext = useMaybeLayoutContext()
React.useEffect(() => {
@@ -80,8 +83,26 @@ export function ControlBar({
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
variation ??= defaultVariation
const showIcon = React.useMemo(
() => variation === 'minimal' || variation === 'verbose',
[variation]
)
const showText = React.useMemo(
() => variation === 'textOnly' || variation === 'verbose',
[variation]
)
const browserSupportsScreenSharing = supportsScreenSharing()
const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false)
const onScreenShareChange = React.useCallback(
(enabled: boolean) => {
setIsScreenShareEnabled(enabled)
},
[setIsScreenShareEnabled]
)
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
const {
@@ -105,38 +126,72 @@ export function ControlBar({
return (
<div {...htmlProps}>
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Microphone}
showIcon={showIcon}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
>
{showText && t('controls.microphone')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="audioinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Camera}
showIcon={showIcon}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
>
{showText && t('controls.camera')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="videoinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
{browserSupportsScreenSharing && (
<ScreenShareToggle
<TrackToggle
source={Track.Source.ScreenShare}
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
showIcon={showIcon}
onChange={onScreenShareChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
/>
>
{showText &&
t(
isScreenShareEnabled
? 'controls.stopScreenShare'
: 'controls.shareScreen'
)}
</TrackToggle>
)}
<HandToggle />
<ChatToggle />
<ParticipantsToggle />
<OptionsButton />
<LeaveButton />
<DisconnectButton>
{showIcon && <LeaveIcon />}
{showText && t('controls.leave')}
</DisconnectButton>
<StartMediaButton />
</div>
)
@@ -30,12 +30,13 @@ import {
import { ControlBar } from './ControlBar'
import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css'
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { participantsStore } from '@/stores/participants'
import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
import { RecordingIndicator } from '@/features/rooms/livekit/components/RecordingIndicator.tsx'
const LayoutWrapper = styled(
'div',
@@ -172,8 +173,8 @@ export function VideoConference({
])
/* eslint-enable react-hooks/exhaustive-deps */
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
return (
<div className="lk-video-conference" {...props}>
@@ -184,6 +185,7 @@ export function VideoConference({
onWidgetChange={widgetUpdate}
>
<div className="lk-video-conference-inner">
<RecordingIndicator />
<LayoutWrapper>
<div
style={{ display: 'flex', position: 'relative', width: '100%' }}
@@ -223,7 +225,7 @@ export function VideoConference({
messageEncoder={chatMessageEncoder}
messageDecoder={chatMessageDecoder}
/>
{sidePanel && <SidePanel />}
{showParticipants && <ParticipantsList />}
</LayoutWrapper>
<ControlBar />
</div>
@@ -1,8 +1,7 @@
import { Trans, useTranslation } from 'react-i18next'
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
import { logoutUrl, useUser } from '@/features/auth'
import { ProConnectButton } from '@/components/ProConnectButton'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
@@ -29,7 +28,9 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<ProConnectButton />
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
</>
)}
<H lvl={2}>{t('language.heading')}</H>
@@ -4,12 +4,11 @@ import {
usePersistentUserChoices,
useRoomContext,
} from '@livekit/components-react'
import { logoutUrl, useUser } from '@/features/auth'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { css } from '@/styled-system/css'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { HStack } from '@/styled-system/jsx'
import { useState } from 'react'
import { ProConnectButton } from '@/components/ProConnectButton'
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -59,7 +58,9 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<ProConnectButton />
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
</>
)}
<HStack
@@ -1,33 +0,0 @@
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
}
+3 -8
View File
@@ -2,16 +2,13 @@ import { Link } from 'wouter'
import { css } from '@/styled-system/css'
import { Stack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { Text, Button } from '@/primitives'
import { A, Text, Button } from '@/primitives'
import { SettingsButton } from '@/features/settings'
import { logoutUrl, useUser } from '@/features/auth'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
import { Feedback } from '@/components/Feedback'
import { Menu } from '@/primitives/Menu'
import { MenuList } from '@/primitives/MenuList'
import { ProConnectButton } from '@/components/ProConnectButton'
import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { terminateSupportSession } from '@/features/support/hooks/useSupport'
export const Header = () => {
const { t } = useTranslation()
@@ -66,7 +63,7 @@ export const Header = () => {
<nav>
<Stack gap={1} direction="row" align="center">
{isLoggedIn === false && !isHome && (
<ProConnectButton hint={false} />
<A href={authUrl()}>{t('login')}</A>
)}
{!!user && (
<Menu>
@@ -82,8 +79,6 @@ export const Header = () => {
items={[{ value: 'logout', label: t('logout') }]}
onAction={(value) => {
if (value === 'logout') {
terminateAnalyticsSession()
terminateSupportSession()
window.location.href = logoutUrl()
}
}}
+2 -6
View File
@@ -1,5 +1,5 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "",
"cancel": "",
"closeDialog": "",
@@ -12,11 +12,7 @@
},
"loading": "",
"loggedInUserTooltip": "",
"login": {
"buttonLabel": "",
"linkLabel": "",
"link": ""
},
"login": "Anmelden",
"logout": "",
"notFound": {
"heading": ""
+6 -48
View File
@@ -4,27 +4,11 @@
"heading": ""
},
"join": {
"videoinput": {
"choose": "",
"disable": "",
"enable": "",
"label": "",
"placeholder": ""
},
"audioinput": {
"choose": "",
"disable": "",
"enable": "",
"label": ""
},
"camlabel": "",
"heading": "",
"joinLabel": "",
"joinMeeting": "",
"toggleOff": "",
"toggleOn": "",
"userLabel": "",
"usernameHint": "",
"usernameLabel": ""
"micLabel": "",
"userLabel": ""
},
"leaveRoomPrompt": "",
"shareDialog": {
@@ -53,10 +37,6 @@
"raise": "",
"lower": ""
},
"screenShare": {
"start": "",
"stop": ""
},
"leave": "",
"participants": {
"open": "",
@@ -69,35 +49,13 @@
"feedbacks": "",
"support": "",
"settings": "",
"username": "",
"effects": ""
"username": ""
}
},
"effects": {
"activateCamera": "",
"notAvailable": "",
"heading": "",
"blur": {
"light": "",
"normal": "",
"apply": "",
"clear": ""
},
"experimental": ""
},
"sidePanel": {
"heading": {
"participants": "",
"effects": ""
},
"content": {
"participants": "",
"effects": ""
},
"closeButton": ""
},
"participants": {
"heading": "",
"subheading": "",
"closeButton": "",
"contributors": "",
"collapsable": {
"open": "",
+3 -7
View File
@@ -1,10 +1,10 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "Back to homescreen",
"cancel": "Cancel",
"closeDialog": "Close dialog",
"error": {
"heading": "An error occurred while loading the page"
"heading": "An error occured while loading the page"
},
"feedbackAlert": "Give us feedback",
"forbidden": {
@@ -12,11 +12,7 @@
},
"loading": "Loading…",
"loggedInUserTooltip": "Logged in as…",
"login": {
"buttonLabel": "Login with ProConnect",
"linkLabel": "What is ProConnect? - new tab",
"link": "What is ProConnect?"
},
"login": "Login",
"logout": "Logout",
"notFound": {
"heading": "Page not found"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Create a meeting",
"heading": "Welcome in Visio",
"heading": "Welcome in Meet",
"intro": "Work easily, from anywhere.",
"joinInputError": "Use a meeting link or code. Examples:",
"joinInputExample": "URL or 10-letter code",
+10 -50
View File
@@ -1,30 +1,14 @@
{
"feedback": {
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
"heading": "Help us improve Visio"
"heading": "Help us improve Meet"
},
"join": {
"videoinput": {
"choose": "Select camera",
"disable": "Disable camera",
"enable": "Enable camera",
"label": "Camera",
"placeholder": "Enable camera to see the preview"
},
"audioinput": {
"choose": "Select microphone",
"disable": "Disable microphone",
"enable": "Enable microphone",
"label": "Microphone"
},
"heading": "Verify your settings",
"camlabel": "Camera",
"heading": "Join the meeting",
"joinLabel": "Join",
"joinMeeting": "Join meeting",
"toggleOff": "Click to turn off",
"toggleOn": "Click to turn on",
"userLabel": "",
"usernameHint": "Shown to other participants",
"usernameLabel": "Your name"
"micLabel": "Microphone",
"userLabel": "Your name"
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
@@ -43,6 +27,8 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Share screen",
"stopScreenShare": "Stop screen share",
"chat": {
"open": "Close the chat",
"closed": "Open the chat"
@@ -51,10 +37,6 @@
"raise": "Raise hand",
"lower": "Lower hand"
},
"screenShare": {
"start": "Share screen",
"stop": "Stop screen share"
},
"leave": "Leave",
"participants": {
"open": "Hide everyone",
@@ -67,35 +49,13 @@
"feedbacks": "Give us feedbacks",
"support": "Get Help on Tchap",
"settings": "Settings",
"username": "Update Your Name",
"effects": "Apply effects"
"username": "Update Your Name"
}
},
"effects": {
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
"notAvailable": "The blur effect will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome :(",
"heading": "Blur",
"blur": {
"light": "Light blur",
"normal": "Blur",
"apply": "Enable blur",
"clear": "Disable blur"
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effects"
},
"content": {
"participants": "participants",
"effects": "effects"
},
"closeButton": "Hide {{content}}"
},
"participants": {
"heading": "Participants",
"subheading": "In room",
"closeButton": "Hide participants",
"you": "You",
"contributors": "Contributors",
"collapsable": {
+1 -1
View File
@@ -3,7 +3,7 @@
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
"heading": "Account",
"youAreNotLoggedIn": "You are not logged in.",
"nameLabel": "Your Name",
"nameLabel": "Votre Nom",
"authentication": "Authentication"
},
"audio": {
+2 -6
View File
@@ -1,5 +1,5 @@
{
"app": "Visio",
"app": "Meet",
"backToHome": "Retour à l'accueil",
"cancel": "Annuler",
"closeDialog": "Fermer la fenêtre modale",
@@ -12,11 +12,7 @@
},
"loading": "Chargement…",
"loggedInUserTooltip": "Connecté en tant que…",
"login": {
"buttonLabel": "S'identifier avec ProConnect",
"linkLabel": "Qu'est-ce que ProConnect ? - nouvelle fenêtre",
"link": "Qu'est-ce que ProConnect ?"
},
"login": "Se connecter",
"logout": "Se déconnecter",
"notFound": {
"heading": "Page introuvable"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Créer une réunion",
"heading": "Visio",
"heading": "Meet",
"intro": "Collaborez en toute simplicité, où que vous soyez.",
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
+10 -50
View File
@@ -1,30 +1,14 @@
{
"feedback": {
"body": "Remplissez le formulaire disponible dans l'entête du site pour nous donner votre avis sur l'outil. Vos retours sont précieux ! Merci.",
"heading": "Aidez-nous à améliorer Visio"
"heading": "Aidez-nous à améliorer Meet"
},
"join": {
"videoinput": {
"choose": "Choisir la webcam",
"disable": "Désactiver la webcam",
"enable": "Activer la webcam",
"label": "Webcam",
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
},
"audioinput": {
"choose": "Choisir le micro",
"disable": "Désactiver le micro",
"enable": "Activer le micro",
"label": "Microphone"
},
"heading": "Vérifiez vos paramètres",
"camlabel": "Webcam",
"heading": "Rejoindre la réunion",
"joinLabel": "Rejoindre",
"joinMeeting": "Rejoindre la réjoindre",
"toggleOff": "Cliquez pour désactiver",
"toggleOn": "Cliquez pour activer",
"userLabel": "",
"usernameHint": "Affiché aux autres participants",
"usernameLabel": "Votre nom"
"micLabel": "Micro",
"userLabel": "Votre nom"
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
@@ -43,6 +27,8 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Partager l'écran",
"stopScreenShare": "Arrêter le partage",
"chat": {
"open": "Masquer le chat",
"closed": "Afficher le chat"
@@ -51,10 +37,6 @@
"raise": "Lever la main",
"lower": "Baisser la main"
},
"screenShare": {
"start": "Partager l'écran",
"stop": "Arrêter le partage"
},
"leave": "Quitter",
"participants": {
"open": "Masquer les participants",
@@ -67,35 +49,13 @@
"feedbacks": "Partager votre avis",
"support": "Obtenir de l'aide sur Tchap",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Appliquer des effets"
"username": "Choisir votre nom"
}
},
"effects": {
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
"notAvailable": "L'effet de flou sera bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome :(",
"heading": "Flou",
"blur": {
"light": "Léger flou",
"normal": "Flou",
"apply": "Appliquer le flou",
"clear": "Désactiver le flou"
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet sur tous les navigateurs et une meilleur qualité."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effets"
},
"content": {
"participants": "les participants",
"effects": "les effets"
},
"closeButton": "Masquer {{content}}"
},
"participants": {
"heading": "Participants",
"subheading": "Dans la réunion",
"closeButton": "Masquer les participants",
"you": "Vous",
"contributors": "Contributeurs",
"collapsable": {
+4 -5
View File
@@ -134,11 +134,10 @@ export const Checkbox = ({
>
<StyledCheckbox {...props}>
{(renderProps) => {
if (renderProps.isInvalid && !!props.validate) {
setError(props.validate(renderProps.isSelected))
} else {
setError(null)
}
renderProps.isInvalid && !!props.validate
? setError(props.validate(renderProps.isSelected))
: setError(null)
return (
<>
<div className="mt-Checkbox-checkbox">
+1 -1
View File
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
const label = typeof item === 'string' ? item : item.label
return (
<MenuItem
className={menuItemRecipe({ extraPadding: true })}
className={menuItemRecipe()}
key={value}
id={value as string}
onAction={() => {
-10
View File
@@ -1,10 +0,0 @@
import { styled } from '@/styled-system/jsx'
import { Separator as RACSeparator } from 'react-aria-components'
export const Separator = styled(RACSeparator, {
base: {
height: '1px',
background: 'gray.400',
margin: '4px 0',
},
})
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
egressId: string | undefined
}
export const egressStore = proxy<State>({
egressId: undefined,
})
-2
View File
@@ -2,10 +2,8 @@ import { proxy } from 'valtio'
type State = {
showHeader: boolean
sidePanel: 'participants' | 'effects' | null
}
export const layoutStore = proxy<State>({
showHeader: false,
sidePanel: null,
})
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
showParticipants: boolean
}
export const participantsStore = proxy<State>({
showParticipants: false,
})
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.6"
tag: "v0.1.5"
backend:
migrateJobAnnotations:
@@ -108,7 +108,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.6"
tag: "v0.1.5"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.6"
tag: "v0.1.5"
backend:
migrateJobAnnotations:
@@ -109,7 +109,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.6"
tag: "v0.1.5"
ingress:
enabled: true
+10 -22
View File
@@ -1,16 +1,16 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "main"
tag: "hackathon"
backend:
migrateJobAnnotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: http://visio-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: visio-staging.beta.numerique.gouv.fr
DJANGO_ALLOWED_HOSTS: meet-staging.beta.numerique.gouv.fr
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
@@ -42,11 +42,11 @@ backend:
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio-staging.beta.numerique.gouv.fr
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-staging.beta.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://visio-staging.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet-staging.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
@@ -108,32 +108,20 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "main"
tag: "hackathon"
ingress:
enabled: true
host: visio-staging.beta.numerique.gouv.fr
host: meet-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
additional:
- secretName: transitional-tls
hosts:
- {{ .Values.newDomain }}
ingressAdmin:
enabled: true
host: visio-staging.beta.numerique.gouv.fr
host: meet-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
tls:
enabled: true
additional:
- secretName: transitional-tls
hosts:
- {{ .Values.newDomain }}
-55
View File
@@ -1,55 +0,0 @@
{{ 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 }}
-12
View File
@@ -36,13 +36,6 @@ releases:
chart: ./extra
secrets:
- 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
version: {{ .Values.version }}
@@ -71,11 +64,6 @@ environments:
staging:
values:
- 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:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
preprod:
-4
View File
@@ -3,10 +3,6 @@ kind: Secret
metadata:
name: backend
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:
DJANGO_SUPERUSER_EMAIL: {{ .Values.djangoSuperUserEmail }}
DJANGO_SUPERUSER_PASSWORD: {{ .Values.djangoSuperUserPass }}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.6",
"version": "0.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.6",
"version": "0.1.5",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.6",
"version": "0.1.5",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
@@ -14,7 +14,7 @@
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
},
"volta": {
"node": "20.17.0"
"node": "16.15.1"
},
"repository": "https://github.com/numerique-gouv/meet",
"author": "DINUM",