Compare commits

...

51 Commits

Author SHA1 Message Date
lebaudantoine cffd658825 🩹(sdk) notify iframe parent when room is gathered through callback
On the first room creation, when the authentication redirection takes
place, the iframe wasn't properly messaging the parent.
Fixed it. Send the room data in any of the two methods to acquire it.

Actually, the code when gathering data through the callback endpoint is
a bit dirty.
2025-04-04 13:18:17 +02:00
lebaudantoine 3b1b644ef9 ⬆️(frontend) upgrade vite dependency
Necessary for security reasons, CVE-2025-30208.
2025-04-04 11:35:38 +02:00
lebaudantoine ab7a0e1ec2 ⬆️(sdk) upgrade vite dependency in consumer and library
Necessary for security reasons.
2025-04-04 11:35:38 +02:00
renovate[bot] 86a4d68daa ⬆️(dependencies) update django to v5.1.8 [SECURITY] 2025-04-04 10:44:07 +02:00
lebaudantoine de3df889ba (frontend) move support options to menu for a clearer UI
Relocated support options to the menu to keep the interface concise
and reduce visual clutter.
2025-04-04 10:43:51 +02:00
lebaudantoine 7c9fc15359 🚸(frontend) delay tooltip appearance in lateral menu
Improve UX by adding a delay before showing tooltips in the lateral menu.
Users often hover from bottom to top, causing tooltips to overlap other
options too quickly.
2025-04-04 10:43:51 +02:00
lebaudantoine a6a67f2752 🍱(readme) update readme to rename Visio to La Suite Meet
Visio is a MS product. In the official documentation call
our product La Suite Meet.
2025-04-03 23:34:49 +02:00
lebaudantoine 00eb52cd3f 🩹(summary) fix bold markdown syntax
Wrong syntaxt to handle bold, instead was italic.
2025-04-03 23:34:36 +02:00
lebaudantoine 6324be9fd3 🩹(frontend) fix missing notification when state is clear
Notify iframe's parent while integrating Visio when state
is cleared. Requested by Eleonore.
2025-04-03 23:33:23 +02:00
lebaudantoine 66f307b7e8 🐛(frontend) rework the calendar integration approach
Use a totally different strategy, which hopefully works in prod env.
Actually broadcast channel cannot be shared between two different
browsing context, even on a same domain, an iFrame and a pop up
cannot communicate.

Moreover, in an iFrame session cookie are unavailable.
Rely on a newly created backend endpoint to pass room data.
2025-04-02 12:31:04 +02:00
lebaudantoine 506b3978e1 🧑‍💻(frontend) move dev tool to the bottom left corner
Nothing happens in the bottom left corner.
Avoid hiding features while developing.
2025-04-02 12:31:04 +02:00
lebaudantoine db65aef56e 💄(frontend) introduc smNote text variant
Necessary for room information in calendar iframe.
2025-04-02 12:31:04 +02:00
lebaudantoine 4e1a4be650 (backend) introduce a creation callback endpoint
Necessary in cross browser context situation, where we need to
pass data of a room newly created between two different windows.

This happens in Calendar integration.
2025-04-02 12:31:04 +02:00
lebaudantoine 1f603ce17b 🔖(minor) bump release to 0.1.17
April fool by @arnaud-robin.
2025-03-31 23:49:04 +02:00
Arnaud Robin 672ca7dfe4 🚨(frontend) fix linting errors
- Fix linting errors in FaceLandmarksProcessor
- Fix linting errors in EffectsConfiguration
- Fix linting errors in useHasFaceLandmarksAccess
2025-03-31 22:57:02 +02:00
Arnaud Robin 7405011cd2 (frontend) add french touch effect
Enhanced the FaceLandmarksProcessor to include
a new 'french' effect, allowing users to add
a beret image to detected faces. Updated the
EffectsConfiguration component to toggle this
effect and modified localization files to
reflect the change from 'mustache' to 'french'.
2025-03-31 22:57:02 +02:00
Arnaud Robin 1d5aebcfdc (frontend) add mustache effects
Added functionality to display mustache effects
on detected faces. Enhanced the EffectsConfiguration
component to toggle these effects and updated
localization files for this effects.
2025-03-31 22:57:02 +02:00
Arnaud Robin 0d6881382b (frontend) add face landmarks flag
Introduced a new hook, useHasFaceLandmarksAccess
to manage access to face landmark features
based on posthog feature flags.
2025-03-31 22:57:02 +02:00
Arnaud Robin c428f39456 (frontend) add thug glasses
Added functionality to draw glasses on detected
faces by calculating eye positions and scaling
the glasses image accordingly. Initialized glasses
image in the constructor for improved visual
effects during face tracking.
2025-03-31 22:57:02 +02:00
Arnaud Robin cd9b80b966 (frontend) add face tracking
Implement face landmark processor
to be able to track face live.
2025-03-31 22:57:02 +02:00
Florent f80603b4bb 🐛(licence) fix the copyright years in licence
The project has started in 2024 and it is still active in 2025.
2025-03-29 20:30:32 +01:00
Berry den Hartog 3299ae7c39 📝(docs) describe environmental options for meet backend 2025-03-28 20:14:32 +01:00
Jacques ROUSSEL 93ca4f2bf4 🐛(ci) use github action for argocd webhook notification
In order to refactor this notification between alls projetcs, we
chooseto use a custom github action
2025-03-28 16:24:17 +01:00
lebaudantoine 19a4e442c5 🔖(minor) bump release to 0.1.16
Misc fixes, release diarization.
2025-03-25 23:27:41 +01:00
lebaudantoine 53f1c9c1d4 💥(summary) replace Whisper with WhisperX for transcription
Switch from insanely-fast-whisper to WhisperX for transcription services,
gaining word-level granularity not available with FlashAttention in previous
API. Whisper was packaged in FastAPI image with basic diarization and
speaker-segment association capabilities.

Implementation prioritized speed to market over testing. Future iterations
will enhance diarization quality and add proper test coverage. API consumers
should expect behavioral differences and new capabilities.
2025-03-25 17:09:13 +01:00
lebaudantoine 28538b63da (backend) add env variable to configure marketing service timeout
This change allows the marketing service timeout to be easily adjusted
via an environment variable, eliminating the need for a new software release.
Additionally, the update makes the code more explicit and easier to maintain.
2025-03-25 15:41:47 +01:00
renovate[bot] 94ae5d52c2 ⬆️(dependencies) update js dependencies
Updated the useUser hook to avoid unwrapping the query object directly,
which leads to excessive re-rendering.
2025-03-25 14:53:15 +01:00
lebaudantoine 6b38a3c996 💄(frontend) center titles in prejoin components
In non-French languages, the title may be long enough
to span two lines. Ensure the text is centered.
2025-03-25 13:38:10 +01:00
lebaudantoine 0af30ec366 (backend) notify participants only if the room exists
Improves sendData reliability by preventing execution when the room
doesn’t exist.

This change addresses errors in staging and production where waiting
participants arrive before the room owner creates the room.

In remote environments, the LiveKit Python SDK doesn’t return a clean
Twirp error when the room is missing; instead of a proper "server unknown"
response, it raises a ContentTypeError, as if the LiveKit server weren’t
responding with a JSON payload, even though the code specifies otherwise.

While the issue cannot be reproduced locally,
this should help mitigate production errors.

Part of a broader effort to enhance data transmission reliability.

Importantly, a participant requesting entry to a room before the owner
arrives should not be considered an exception.
2025-03-25 13:33:47 +01:00
lebaudantoine 33e017464f 📌(livekit) pin livekit dev version
Pin the LiveKit server version to use the same as the staging
and production environment. It's a good practice.
2025-03-25 13:18:44 +01:00
lebaudantoine 9e91bbe417 🔇(backend) reduce noisy logs in Sentry
Ignore logs from DockerflowMiddleware
and minimize unnecessary log submissions.
2025-03-25 09:42:35 +01:00
renovate[bot] e821982353 ⬆️(dependencies) update python dependencies 2025-03-24 20:04:15 +01:00
lebaudantoine 18f4a117ab 🩹(backend) invert operation order in participant handling
Invert operation sequence to first notify people in room before setting
participant in cache. Fixes infinite loop issue caused by 3s cache timeout
for waiting participants when requests take too long. Problem only occurred
when notifications were delayed, as faster notification delivery masked the
race condition.
2025-03-24 19:39:13 +01:00
lebaudantoine b5b99d4c52 🐛(frontend) fix effect menu scroll
Since refactoring, the effect side panel, was not scrollable anymore.
Fixed it.
2025-03-24 17:10:50 +01:00
lebaudantoine 8a4323aa82 🐛(license) fix reuse.software missing support in GitHub
Duplicate the MIT License, which are not yet supported by GitHub and its badges.
2025-03-24 14:54:42 +01:00
lebaudantoine 5cbac23f13 💄(frontend) adjust minor visual item
Adjust few items from the landing and the interface.
2025-03-24 14:38:27 +01:00
lebaudantoine d55810784d 🐛(frontend) fix Marianne import
The font face definition wasn't working properly. Switch to vanilla css.
2025-03-24 14:38:27 +01:00
lebaudantoine c2fdefe7b2 💄(frontend) adjust font size based on Marianne recent changes
Marianne is bigger than OpenSans, adjust admin side panel style.
2025-03-24 13:53:41 +01:00
lebaudantoine bf2474d0e1 🌐(frontend) add missing translation
The admin side panel was translated too quickly.
I forgot a title, fix it.
2025-03-24 13:53:41 +01:00
lebaudantoine 558d844ef3 ✏️(frontend) fix typos in the admin side panel
Based on Users' feedbacks. Also based on Arnaud's one, avoid
having a double negation for the public restriction.
2025-03-24 13:36:03 +01:00
lebaudantoine 94aa828d87 🌐(frontend) add missing dutch content
Oopsi, forgot some dutch content when resolving conflicts
while merging traductions. Fixed.
2025-03-21 16:59:14 +01:00
lebaudantoine 110e062e56 💄(frontend) use official Marianne font
Updated styling system to implement the official French font used across all
La Suite products. This enhances brand consistency and improves the visual
identity alignment with other government digital services.
2025-03-21 16:57:33 +01:00
lebaudantoine f7c2beb30d 🚸(frontend) add screen sharing troubleshooting modal and setup guide
Created modal that appears when users fail to share their entire screen. Also
added documentation helper that explains proper screen sharing setup steps for
different operating systems and browsers.
2025-03-21 15:27:50 +01:00
lebaudantoine 5bf1456200 ⚰️(secrets) remove useless submodule
Secret are not in-use anymore in the project, dead code.
2025-03-21 15:25:37 +01:00
Ikko Eltociear Ashimine 80e5c9bb95 ✏️(frontend) update BackgroundBlurTrackProcessorJsWrapper.ts
accross -> across
2025-03-18 20:07:26 +01:00
Rust Saiargaliev 74164b8498 🗑️(backend) drop obsolete code from the initial boilerplate
Related to 5b1a2b20de
There are no references to the `generate_document.html` template
in the codebase. The same goes for the `INVITATION_VALIDITY_DURATION` setting,
which arrived straigt from https://github.com/suitenumerique/docs

WeasyPrint is (I believe) not used in the project, so it is a ghost dependency.
2025-03-18 20:05:31 +01:00
Juergen Ehrensberger 82cc2e5d17 📝(helm) correct helm chart repository URL for meet
The URL https://numerique-gouv.github.io/meet/ is not accessible.
The correct URL appears to be https://suitenumerique.github.io/meet/
2025-03-18 20:03:45 +01:00
lebaudantoine fa1503db0d 📄(legal) update legal notice following expert review
following legal team's advices, updates our legal terms.
2025-03-16 18:10:40 +01:00
lebaudantoine e106415740 🔒️(frontend) update meet-frontend image to address security vuls
Fixed two HIGH severity vulnerabilities in libxslt:
- CVE-2024-55549: Use-After-Free in libxslt (xsltGetInheritedNsList)
- CVE-2025-24855: Use-After-Free in libxslt numbers.c

The image was manually updated as no more recent unprivileged nginx-based
images were available. This addresses the security scan failures from Trivy.
2025-03-16 16:36:01 +01:00
lebaudantoine 4199328a4a 📄(license) update license following expert review
following @bzg advice, updates our licenses.
2025-03-15 14:14:41 +01:00
aleb_the_flash 57715fea73 📝(readme) integrate feedbacks from open-source specialist
Clarify the license, and avoid any enterprise-like vocabulary or expression.

Thx @bzg
2025-03-15 14:02:16 +01:00
109 changed files with 4166 additions and 4124 deletions
+6 -9
View File
@@ -119,12 +119,9 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Call argocd github webhook
run: |
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/numerique-gouv/lasuite-deploiement"}}'
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify
with:
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
argocd_webhook_secret: "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}"
argocd_url: "${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}"
-3
View File
@@ -1,3 +0,0 @@
[submodule "secrets"]
path = secrets
url = ../secrets
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Direction Interministérielle du Numérique - Gouvernement Français
Copyright (c) 2024-2025 DINUM/Etalab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+79
View File
@@ -0,0 +1,79 @@
# LICENCE OUVERTE 2.0/OPEN LICENCE 2.0
## Réutilisation de l’« Information » sous cette licence
Le « Concédant » concède au « Réutilisateur » un droit non exclusif et gratuit de libre « Réutilisation » de l’« Information » objet de la présente licence, à des fins commerciales ou non, dans le monde entier et pour une durée illimitée, dans les conditions exprimées ci-dessous.
**Le « Réutilisateur » est libre de réutiliser l’« Information » :**
- de la communiquer, la reproduire, la copier ;
- de ladapter, la modifier, lextraire et la transformer, notamment pour créer des « Informations dérivées » ;
- de la diffuser, la redistribuer, la publier et la transmettre, de lexploiter à titre commercial, par exemple en la combinant avec dautres informations, ou en lincluant dans votre propre produit ou application.
**Sous réserve de :**
- mentionner la paternité de l’«Information» : sa source (a minima le nom du « Concédant ») et la date de la dernière mise à jour de l’« Information » réutilisée.
Le « Réutilisateur » peut notamment s acquitter de cette condition en indiquant ladresse (URL) renvoyant vers « lInformation » et assurant une mention effective de sa paternité.
**Par exemple :**
Dans le cas dune réutilisation de la base SIRENE de lINSEE, mentionner lURL du « Concédant » : www.insee.fr + la date de dernière mise à jour de lInformation réutilisée.
Cette mention de paternité ne doit ni conférer un caractère officiel à la « Réutilisation » de l’« Information », ni suggérer une quelconque reconnaissance ou caution par le « Concédant », ou par toute autre entité publique, du « Réutilisateur » ou de sa « Réutilisation ».
## Données à caractère personnel
L’« Information » mise à disposition peut contenir des « Données à caractère personnel » pouvant faire lobjet dune « Réutilisation ». Alors, le « Concédant » informe le « Réutilisateur » (par tous moyens) de leur présence, l « Information » peut être librement réutilisée, sans faire obstacle aux libertés accordées par la présente licence, à condition de respecter le cadre légal relatif à la protection des données à caractère personnel.
## Droits de propriété intellectuelle
Il est garanti au « Réutilisateur » que l « Information » ne contient pas de « Droits de propriété intellectuelle » appartenant à des tiers qui pourraient faire obstacle aux libertés qui lui sont accordées par la présente licence.
Les éventuels « Droits de propriété intellectuelle » détenus par le « Concédant » sur l « Information » ne font pas obstacle aux libertés qui sont accordées par la présente licence. Lorsque le « Concédant » détient des « Droits de propriété intellectuelle » » sur l « Information », il les cède au « Réutilisateur » de façon non exclusive, à titre gracieux, pour le monde entier, pour toute la durée des « Droits de propriété intellectuelle », et le « Réutilisateur » peut en faire tout usage conformément aux libertés et aux conditions définies par la présente licence.
## Responsabilité
L «Information» est mise à disposition telle que produite ou reçue, sans autre garantie expresse ou tacite qui nest pas prévue par la présente licence. Labsence de défauts ou derreurs éventuellement contenues dans l «Information», comme la fourniture continue de l « Information » nest pas garantie par le «Concédant». Il ne peut être tenu pour responsable de toute perte, préjudice ou dommage de quelque sorte causé à des tiers du fait de la « Réutilisation ».
Le « Réutilisateur » est seul responsable de la « Réutilisation » de l’« Information ».
La « Réutilisation » ne doit pas induire en erreur des tiers quant au contenu de l’« Information », sa source et sa date de mise à jour.
## Droit applicable
La présente licence est régie par le droit français.
### Compatibilité de la présente licence
Elle a été conçue pour être compatible avec toute licence libre qui exige _a minima_ la mention de paternité. Elle est notamment compatible avec la version antérieure de la présente licence ainsi quavec les licences « Open Government Licence » (OGL) du Royaume-Uni, « Creative Commons Attribution » (CC-BY) de Creative Commons et « Open Data Commons Attribution » (ODC-BY) de lOpen Knowledge Foundation.
## Définitions
Sont considérés, au sens de la présente licence comme :
- Le « **Concédant** » : toute personne concédant un droit de « Réutilisation » sur l’« Information » dans les libertés et les conditions prévues par la présente licence.
- L’« **Information** » :
- toute information publique figurant dans des documents communiqués ou publiés par une administration mentionnée au premier alinéa de larticle L.300-2 du CRPA ;
- toute information mise à disposition par toute personne selon les termes et conditions de la présente licence.
- La « **Réutilisation** » : lutilisation de l’« Information » à dautres fins que celles pour lesquelles elle a été produite ou reçue.
- Le « **Réutilisateur** » : toute personne qui réutilise les « Informations » conformément aux conditions de la présente licence.
- Des « **Données à caractère personnel** » : toute information se rapportant à une personne physique identifiée ou identifiable, pouvant être identifiée directement ou indirectement. Leur « Réutilisation » est subordonnée au respect du cadre juridique en vigueur.
- Une « **Information dérivée** » : toute nouvelle donnée ou information créées directement à partir de l’« Information » ou à partir dune combinaison de l « Information » et dautres données ou informations non soumises à cette licence.
- Les « **Droits de propriété intellectuelle** » : tous droits identifiés comme tels par le Code de la propriété intellectuelle (droit dauteur, droits voisins au droit dauteur, droit sui generis des producteurs de bases de données).
## À propos de cette licence
La présente licence a vocation à être utilisée par les administrations pour la réutilisation de leurs informations publiques. Elle peut également être utilisée par toute personne souhaitant mettre à disposition de l’« Information » dans les conditions définies par la présente licence
La France est dotée dun cadre juridique global visant à une diffusion spontanée par les administrations de leurs informations publiques afin den permettre la plus large réutilisation.
Le droit de la « Réutilisation » de l’« Information » des administrations est régi par le code des relations entre le public et ladministration (CRPA) et, le cas échéant, le code du patrimoine (livre II relatif aux archives).
Cette licence facilite la réutilisation libre et gratuite des informations publiques et figure parmi les licences qui peuvent être utilisées par ladministration en vertu du décret pris en application de larticle L.323-2 du CRPA.
Etalab est la mission chargée, sous lautorité du Premier ministre, douvrir le plus grand nombre de données publiques des administrations de l’État et de ses établissements publics. Elle a réalisé la Licence Ouverte pour faciliter la réutilisation libre et gratuite de ces informations publiques, telles que définies par larticle L321-1 du CRPA.
Cette licence est une version 2.0 de la Licence Ouverte.
Etalab se réserve la faculté de proposer de nouvelles versions de la Licence Ouverte. Cependant, les « Réutilisateurs » pourront continuer à réutiliser les informations disponibles sous cette licence sils le souhaitent.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-2025 DINUM/Etalab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+25 -19
View File
@@ -1,5 +1,5 @@
<p align="center">
<img alt="posthoglogo" src="./docs/assets/visio-logo.png" maxWidth="100%">
<img alt="meet logo" src="./docs/assets/banner-meet-fr.png" maxWidth="100%">
</p>
@@ -21,13 +21,13 @@
<p align="center">
<a href="https://visio.numerique.gouv.fr/">
<img src="https://github.com/user-attachments/assets/09c1faa1-de88-4848-af3a-6fbe793999bf" alt="Visio Demonstration">
<img src="https://github.com/user-attachments/assets/09c1faa1-de88-4848-af3a-6fbe793999bf" alt="La Suite Meet Demonstration">
</a>
</p>
## Visio: Simple Video Conferencing
## La Suite Meet: Simple Video Conferencing
Powered by [LiveKit](https://livekit.io/), Visio offers Zoom-level performance with high-quality video and audio. No installation required—simply join calls directly from your browser. Check out LiveKit's impressive optimizations in their [blog post](https://blog.livekit.io/livekit-one-dot-zero/).
Powered by [LiveKit](https://livekit.io/), La Suite Meet offers Zoom-level performance with high-quality video and audio. No installation required—simply join calls directly from your browser. Check out LiveKit's impressive optimizations in their [blog post](https://blog.livekit.io/livekit-one-dot-zero/).
### Features
- Optimized for stability in large meetings (+100 p.)
- Support for multiple screen sharing streams
@@ -45,47 +45,47 @@ Powered by [LiveKit](https://livekit.io/), Visio offers Zoom-level performance w
- SVC codecs (VP9, AV1)
Visio is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's free to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
La Suite Meet is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's simple to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
Were continuously adding new features to enhance your experience, with the latest updates coming soon!
## Table of Contents
- [Get started for free](#get-started-for-free)
- [Get started](#get-started)
- [Docs](#docs)
- [Contributing](#contributing)
- [Philosophy](#philosophy)
- [Open-source](#open-source)
- [Open source](#open-source)
## Get started for free
## Get started
### Visio Cloud (Recommended)
Sign up for Visio Cloud, designed for European public servants. Hosted on SecNumCloud-compliant providers and accessible via government SSO, [ProConnect](https://www.proconnect.gouv.fr/). Start for free. Reach out if your entity isn't connected yet to our sso.
### La Suite Meet Cloud (Recommended)
Sign up for La Suite Meet Cloud, designed for french public servants. Hosted on SecNumCloud-compliant providers and accessible via government SSO, [ProConnect](https://www.proconnect.gouv.fr/). The easiest way to try our product. Reach out if your entity isn't connected yet to our sso.
### Open-source deploy (Advanced)
### Open-source deployment (Advanced)
Deploy Visio on your own infrastructure using [our self-hosting guide](https://github.com/suitenumerique/meet/blob/main/docs/installation.md). Our open-source deployment is optimized for Kubernetes, and we're working on supporting additional deployment options. Keycloak integration and any SSO are supported. We offer customer support for open-source setups—just reach out for assistance.
Deploy La Suite Meet on your own infrastructure using [our self-hosting guide](https://github.com/suitenumerique/meet/blob/main/docs/installation.md). Our open-source deployment is optimized for Kubernetes, and we're working on supporting additional deployment options. Keycloak integration and any SSO are supported. We offer customer support for open-source setups—just reach out for assistance.
## Docs
We're currently working on both technical and user documentation for Visio. In the meantime, many of the essential aspects are already well covered by the [LiveKit documentation](https://docs.livekit.io/home/) and their [self-hosting guide](https://docs.livekit.io/home/self-hosting/deployment/). Stay tuned for more updates!
We're currently working on both technical and user documentation for La Suite Meet. In the meantime, many of the essential aspects are already well covered by the [LiveKit documentation](https://docs.livekit.io/home/) and their [self-hosting guide](https://docs.livekit.io/home/self-hosting/deployment/). Stay tuned for more updates!
## Contributing
We <3 contributions big and small:
We <3 contributions of any kind, big and small:
- Vote on features or get early access to beta functionality in our [roadmap](https://github.com/orgs/suitenumerique/projects/3/views/2)
- Open a PR (see our instructions on [developing Visio locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md))
- Open a PR (see our instructions on [developing La Suite Meet locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md))
- Submit a [feature request](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=enhancement&template=Feature_request.md) or [bug report](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md)
## Philosophy
Were relentlessly focused on building the best open-source video conferencing product—Visio. Growth comes from creating something people truly need, not just from chasing metrics.
Were relentlessly focused on building the best open-source video conferencing product—La Suite Meet. Growth comes from creating something people truly need, not just from chasing metrics.
Our users come first. Were committed to making Visio as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
Our users come first. Were committed to making La Suite Meet as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
Most of the heavy engineering is handled by the incredible LiveKit team, allowing us to focus on delivering a top-tier product. We follow extreme programming practices, favoring pair programming and quick, iterative releases. Challenge our tech and architecture—simplicity is always our top priority.
@@ -94,12 +94,12 @@ Most of the heavy engineering is handled by the incredible LiveKit team, allowin
Gov 🇫🇷 supports open source! This project is available under [MIT license](https://github.com/suitenumerique/meet/blob/0cc2a7b7b4f4821e2c4d9d790efa739622bb6601/LICENSE).
All features we develop will always remain free, and we are committed to contributing back to the LiveKit community whenever feasible.
All features we develop will always remain open-source, and we are committed to contributing back to the LiveKit community whenever feasible.
To learn more, don't hesitate to [reach out](mailto:visio@numerique.gouv.fr).
### Help us!
Come help us make Visio even better. We're growing fast and [would love some help](mailto:visio@numerique.gouv.fr).
Come help us make La Suite Meet even better. We're growing fast and [would love some help](mailto:visio@numerique.gouv.fr).
## Contributors 🧞
@@ -111,3 +111,9 @@ Come help us make Visio even better. We're growing fast and [would love some hel
## Credits
We're using the awesome [LiveKit](https://livekit.io/) implementation. We're also thankful to the teams behind [Django Rest Framework](https://www.django-rest-framework.org/), [Vite.js](https://vite.dev/), and [React Aria](https://github.com/adobe/react-spectrum) — Thanks for your amazing work!
## License
Code in this repository is published under the MIT license by DINUM (Direction interministériel du numérique).
Documentation (in the docs/) directory is released under the [Etalab-2.0 license](https://spdx.org/licenses/etalab-2.0.html).
+1 -1
View File
@@ -1,4 +1,4 @@
FROM livekit/livekit-server:latest
FROM livekit/livekit-server:v1.8.0
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
COPY rootCA.pem /etc/ssl/certs/
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

+107 -10
View File
@@ -2,7 +2,6 @@
This document is a step-by-step guide that describes how to install Visio on a k8s cluster without AI features.
## Prerequisites
- k8s cluster with an nginx-ingress controller
@@ -43,11 +42,11 @@ It will expire on 23 March 2027 🗓
2. Create kind cluster with containerd registry config dir enabled
Creating cluster "visio" ...
✓ Ensuring node image (kindest/node:v1.27.3) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-visio"
You can now use your cluster with:
@@ -95,13 +94,14 @@ ingress-nginx-admission-create-jgnc9 0/1 Completed 0 2m44s
ingress-nginx-admission-patch-wrt47 0/1 Completed 0 2m44s
ingress-nginx-controller-57c548c4cd-9xwt6 1/1 Running 0 2m44s
```
When your k8s cluster is ready, you can start the deployment. This cluster is special because it uses the *.127.0.0.1.nip.io domain and mkcert certificates to have full HTTPS support and easy domain name management.
Please remember that *.127.0.0.1.nip.io will always resolve to 127.0.0.1, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
When your k8s cluster is ready, you can start the deployment. This cluster is special because it uses the \*.127.0.0.1.nip.io domain and mkcert certificates to have full HTTPS support and easy domain name management.
Please remember that \*.127.0.0.1.nip.io will always resolve to 127.0.0.1, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
## Preparation
### What will you use to authenticate your users ?
### What will you use to authenticate your users ?
Visio uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus Visio) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
@@ -153,6 +153,7 @@ keycloak-0 1/1 Running 0 26m
keycloak-postgresql-0 1/1 Running 0 26m
redis-master-0 1/1 Running 0 35s
```
When the redis is ready we can deploy livekit-server.
```
@@ -194,6 +195,7 @@ livekit-livekit-server-5c5fb87f7f-ct6x5 1/1 Running 0 15m
postgresql-0 1/1 Running 0 50s
redis-master-0 1/1 Running 0 19
```
From here important information you will need are :
```
@@ -212,7 +214,7 @@ POSTGRES_PASSWORD: pass
Now you are ready to deploy Visio without AI. AI required more dependencies (Openai-compliant API, LiveKit Egress, Cold storage and a docs deployment to push resumes). To deploy meet you need to provide all previous information to the helm chart.
```
$ helm repo add meet https://numerique-gouv.github.io/meet/
$ helm repo add meet https://suitenumerique.github.io/meet/
$ helm repo update
$ helm install meet meet/meet -f examples/meet.values.yaml
```
@@ -231,3 +233,98 @@ meet-admin <none> meet.127.0.0.1.nip.io localhost 80, 44
```
You can use Visio on https://meet.127.0.0.1.nip.io. The provisioning user in keycloak is meet/meet.
## All options
These are the environmental options available on meet backend.
| Option | Description | default |
| ----------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DJANGO_ALLOWED_HOSTS | Host that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | | false |
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
| DB_NAME | name of the database | meet |
| DB_USER | user used to connect to database | dinum |
| DB_PASSWORD | password used to connect to the database | pass |
| DB_HOST | hostname of the database | localhost |
| DB_PORT | port to connect to database | 5432 |
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
| AWS_S3_ACCESS_KEY_ID | s3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | s3 secret key | |
| AWS_S3_REGION_NAME | s3 region | |
| AWS_STORAGE_BUCKET_NAME | s3 bucket name | |
| DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | redis endpoint | redis://redis:6379/1 |
| REQUEST_ENTRY_THROTTLE_RATES | throttle rates | 150/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | deploy check | False |
| FRONTEND_ANALYTICS | analytics information | {} |
| FRONTEND_SUPPORT | crisp frontend support | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | silence livekit debug | false |
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST | host of the email server | |
| DJANGO_EMAIL_HOST_USER | user to connect to the email server | |
| DJANGO_EMAIL_HOST_PASSWORD | password to connect tto the email server | |
| DJANGO_EMAIL_PORT | por tot connect to the email server | |
| DJANGO_EMAIL_USE_TLS | enable tls on email connection | false |
| DJANGO_EMAIL_USE_SSL | enable tls on email connection | false |
| DJANGO_EMAIL_FROM | email from account | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all cors origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | origins to allow in string | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | origins to allow in regex | [] |
| SENTRY_DSN | sentry server | |
| DJANGO_CELERY_BROKER_URL | celery broker host | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker options | {} |
| OIDC_CREATE_USER | create oidc user if not exists | false |
| OIDC_VERIFY_SSL | verify ssl for oidc | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | fallback to email for identification | false |
| OIDC_RP_SIGN_ALGO | token verification algoritm used by oidc | RS256 |
| OIDC_RP_CLIENT_ID | oidc client | meet |
| OIDC_RP_CLIENT_SECRET | oidc client secret | |
| OIDC_OP_JWKS_ENDPOINT | oidc endpoint for JWKS | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | |
| OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | |
| OIDC_OP_USER_ENDPOINT | oidc endpoint for user | |
| OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | |
| OIDC_RP_SCOPES | oidc scopes | openid email |
| LOGIN_REDIRECT_URL | login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | login redurect url for failure | |
| LOGOUT_REDIRECT_URL | url to redirect to on logout | |
| OIDC_USE_NONCE | use nonce for oidc | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | require https for oidc | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | allowed redirect hosts for oidc | [] |
| OIDC_STORE_ID_TOKEN | store oidc ID token | true |
| ALLOW_LOGOUT_GET_METHOD | allow logout through get method | true |
| OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo |
| OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name |
| LIVEKIT_API_KEY | livekit api key | |
| LIVEKIT_API_SECRET | livekit api secret | |
| LIVEKIT_API_URL | livekit api url | |
| RESOURCE_DEFAULT_ACCESS_LEVEL | default resource access level for rooms | public |
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
| RECORDING_ENABLE | record meeting option | false |
| RECORDING_OUTPUT_FOLDER | folder to store meetings | recordings |
| RECORDING_VERIFY_SSL | verify ssl for recording storage | true |
| RECORDING_WORKER_CLASSES | worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EVENT_PARSER_CLASS | storage event engine for recording | core.recording.event.parsers.MinioParser |
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | enable storage event authorization | true |
| RECORDING_STORAGE_EVENT_ENABLE | enable recording storage events | false |
| RECORDING_STORAGE_EVENT_TOKEN | recording storage event token | |
| SUMMARY_SERVICE_ENDPOINT | summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | api token voor summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | markering class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | breva api key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | brevo api contact list ids | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | brevo contact attributes | {"VISIO_USER": True} |
| BREVO_API_TIMEOUT | brevo timeout | 1 |
| LOBBY_KEY_PREFIX | lobby prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | lobby waiting tumeout | 3 |
| LOBBY_DENIED_TIMEOUT | lobby deny timeout | 5 |
| LOBBY_ACCEPTED_TIMEOUT | lobby accept timeout | 21600 |
| LOBBY_NOTIFICATION_TYPE | lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | lobby cookie name | lobbyParticipantId |
Submodule secrets deleted from 2ba12db71d
+14
View File
@@ -209,3 +209,17 @@ class ParticipantEntrySerializer(serializers.Serializer):
def update(self, instance, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
class CreationCallbackSerializer(serializers.Serializer):
"""Validate room creation callback data."""
callback_id = serializers.CharField(required=True)
def create(self, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
def update(self, instance, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
+35
View File
@@ -47,6 +47,7 @@ from core.services.lobby import (
LobbyParticipantNotFound,
LobbyService,
)
from core.services.room_creation import RoomCreation
from . import permissions, serializers
@@ -186,6 +187,12 @@ class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
scope = "request_entry"
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
"""Throttle Anonymous user requesting room generation callback"""
scope = "creation_callback"
class RoomViewSet(
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
@@ -268,6 +275,9 @@ class RoomViewSet(
role=models.RoleChoices.OWNER,
)
if callback_id := self.request.data.get("callback_id"):
RoomCreation().persist_callback_state(callback_id, room)
@decorators.action(
detail=True,
methods=["post"],
@@ -460,6 +470,31 @@ class RoomViewSet(
{"status": "error", "message": str(e)}, status=status_code
)
@decorators.action(
detail=False,
methods=["POST"],
url_path="creation-callback",
permission_classes=[],
throttle_classes=[CreationCallbackAnonRateThrottle],
)
def creation_callback(self, request):
"""Retrieve cached room data via an unauthenticated request with a unique ID.
Designed for interoperability across iframes, popups, and other contexts,
even on the same domain, bypassing browser security restrictions on direct communication.
"""
serializer = serializers.CreationCallbackSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
room = RoomCreation().get_callback_state(
callback_id=serializer.validated_data.get("callback_id")
)
return drf_response.Response(
{"status": "success", "room": room}, status=drf_status.HTTP_200_OK
)
class ResourceAccessListModelMixin:
"""List mixin for resource access API."""
+3 -1
View File
@@ -121,7 +121,9 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
contact_data = ContactData(
email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]}
)
marketing_service.create_contact(contact_data, timeout=1)
marketing_service.create_contact(
contact_data, timeout=settings.BREVO_API_TIMEOUT
)
except (ContactCreationError, ImproperlyConfigured, ImportError):
pass
+23 -7
View File
@@ -12,7 +12,12 @@ from django.conf import settings
from django.core.cache import cache
from asgiref.sync import async_to_sync
from livekit.api import LiveKitAPI, SendDataRequest, TwirpError # pylint: disable=E0611
from livekit.api import ( # pylint: disable=E0611
ListRoomsRequest,
LiveKitAPI,
SendDataRequest,
TwirpError,
)
from core import models, utils
@@ -221,6 +226,12 @@ class LobbyService:
color=color,
)
try:
self.notify_participants(room_id=room_id)
except LobbyNotificationError:
# If room not created yet, there is no participants to notify
pass
cache_key = self._get_cache_key(room_id, participant_id)
cache.set(
cache_key,
@@ -228,12 +239,6 @@ class LobbyService:
timeout=settings.LOBBY_WAITING_TIMEOUT,
)
try:
self.notify_participants(room_id=room_id)
except LobbyNotificationError:
# If room not created yet, there is no participants to notify
pass
return participant
def _get_participant(
@@ -343,7 +348,18 @@ class LobbyService:
}
lkapi = LiveKitAPI(**settings.LIVEKIT_CONFIGURATION)
try:
room_response = await lkapi.room.list_rooms(
ListRoomsRequest(
names=[str(room_id)],
)
)
# Check if the room exists
if not room_response.rooms:
return
await lkapi.room.send_data(
SendDataRequest(
room=str(room_id),
@@ -0,0 +1,37 @@
"""Room creation service."""
from django.conf import settings
from django.core.cache import cache
class RoomCreation:
"""Room creation related methods"""
@staticmethod
def _get_cache_key(callback_id):
"""Generate a standardized cache key for room creation callbacks."""
return f"room-creation-callback_{callback_id}"
def persist_callback_state(self, callback_id: str, room) -> None:
"""Store room data in cache using the callback ID as an identifier."""
data = {
"slug": room.slug,
}
cache.set(
self._get_cache_key(callback_id),
data,
timeout=settings.ROOM_CREATION_CALLBACK_CACHE_TIMEOUT,
)
def get_callback_state(self, callback_id: str) -> dict:
"""Retrieve and clear cached room data for the given callback ID."""
cache_key = self._get_cache_key(callback_id)
data = cache.get(cache_key)
if not data:
return {}
cache.delete(cache_key)
return data
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Generate Document</title>
</head>
<body>
<h2>Generate Document</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Generate PDF</button>
</form>
</body>
</html>
@@ -2,6 +2,9 @@
Test rooms API endpoints in the Meet core app: create.
"""
# pylint: disable=W0621,W0613
from django.core.cache import cache
import pytest
from rest_framework.test import APIClient
@@ -11,6 +14,15 @@ from ...models import Room
pytestmark = pytest.mark.django_db
@pytest.fixture
def reset_cache():
"""Provide cache cleanup after each test to maintain test isolation."""
yield
keys = cache.keys("room-creation-callback_*")
if keys:
cache.delete(*keys)
def test_api_rooms_create_anonymous():
"""Anonymous users should not be allowed to create rooms."""
client = APIClient()
@@ -26,7 +38,7 @@ def test_api_rooms_create_anonymous():
assert Room.objects.exists() is False
def test_api_rooms_create_authenticated():
def test_api_rooms_create_authenticated(reset_cache):
"""
Authenticated users should be able to create rooms and should automatically be declared
as owner of the newly created room.
@@ -49,6 +61,33 @@ def test_api_rooms_create_authenticated():
assert room.slug == "my-room"
assert room.accesses.filter(role="owner", user=user).exists() is True
rooms_data = cache.keys("room-creation-callback_*")
assert not rooms_data
def test_api_rooms_create_generation_cache(reset_cache):
"""
Authenticated users creating a room with a callback ID should have room data stored in cache.
"""
user = UserFactory()
client = APIClient()
client.force_login(user)
response = client.post(
"/api/v1.0/rooms/",
{"name": "my room", "callback_id": "1234"},
)
assert response.status_code == 201
room = Room.objects.get()
assert room.name == "my room"
assert room.slug == "my-room"
assert room.accesses.filter(role="owner", user=user).exists() is True
room_data = cache.get("room-creation-callback_1234")
assert room_data.get("slug") == "my-room"
def test_api_rooms_create_authenticated_existing_slug():
"""
@@ -0,0 +1,98 @@
"""
Test rooms API endpoints in the Meet core app: creation callback functionality.
"""
# pylint: disable=W0621,W0613
from django.core.cache import cache
import pytest
from rest_framework.test import APIClient
from ...factories import UserFactory
pytestmark = pytest.mark.django_db
# Tests for creation_callback endpoint
@pytest.fixture
def reset_cache():
"""Provide cache cleanup after each test to maintain test isolation."""
yield
keys = cache.keys("room-creation-callback_*")
if keys:
cache.delete(*keys)
def test_api_rooms_create_anonymous(reset_cache):
"""Anonymous user can retrieve room data once using a valid callback ID."""
client = APIClient()
cache.set("room-creation-callback_123", {"slug": "my room"})
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
# Data should be cleared after retrieval
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {}}
def test_api_rooms_create_empty_cache():
"""Valid callback ID return empty room data when nothing is stored in the cache."""
client = APIClient()
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {}}
def test_api_rooms_create_missing_callback_id():
"""Requests without a callback ID properly fail with a 400 status code."""
client = APIClient()
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{},
)
assert response.status_code == 400
def test_api_rooms_create_authenticated(reset_cache):
"""Authenticated users can also successfully retrieve room data using a valid callback ID"""
user = UserFactory()
client = APIClient()
client.force_login(user)
cache.set("room-creation-callback_123", {"slug": "my room"})
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
@@ -776,6 +776,43 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
@mock.patch("core.services.lobby.LiveKitAPI")
def test_notify_participants_success_no_room(mock_livekit_api, lobby_service):
"""Test the notify_participants method when the LiveKit room doesn't exist yet."""
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
# Create a proper response object with an empty rooms list
class MockResponse:
"""LiveKit API response mock with empty rooms list."""
rooms = []
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_livekit_api.return_value = mock_api_instance
# Act
lobby_service.notify_participants(room.id)
# Assert
# Verify the API was initialized with correct configuration
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was not called since no room exists
mock_api_instance.room.send_data.assert_not_called()
# Verify the connection was properly closed
mock_api_instance.aclose.assert_called_once()
@mock.patch("core.services.lobby.LiveKitAPI")
def test_notify_participants_success(mock_livekit_api, lobby_service):
"""Test successful participant notification."""
@@ -784,6 +821,14 @@ def test_notify_participants_success(mock_livekit_api, lobby_service):
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_livekit_api.return_value = mock_api_instance
@@ -793,6 +838,9 @@ def test_notify_participants_success(mock_livekit_api, lobby_service):
# Verify the API was called correctly
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was called
mock_api_instance.room.send_data.assert_called_once()
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
@@ -817,6 +865,14 @@ def test_notify_participants_error(mock_livekit_api, lobby_service):
mock_api_instance.room.send_data = mock.AsyncMock(
side_effect=TwirpError(msg="test error", code=123)
)
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_livekit_api.return_value = mock_api_instance
@@ -829,6 +885,9 @@ def test_notify_participants_error(mock_livekit_api, lobby_service):
# Verify the API was called correctly
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify send_data was called
mock_api_instance.room.send_data.assert_called_once()
+19 -1
View File
@@ -19,6 +19,7 @@ from django.utils.translation import gettext_lazy as _
import sentry_sdk
from configurations import Configuration, values
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -271,6 +272,11 @@ class Base(Configuration):
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
environ_prefix=None,
),
"creation_callback": values.Value(
default="600/minute",
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
environ_prefix=None,
),
},
}
@@ -314,7 +320,6 @@ class Base(Configuration):
EMAIL_FROM = values.Value("from@example.com")
AUTH_USER_MODEL = "core.User"
INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds
# CORS
CORS_ALLOW_CREDENTIALS = True
@@ -498,6 +503,9 @@ class Base(Configuration):
converter=lambda x: int(x), # pylint: disable=unnecessary-lambda
)
BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True})
BREVO_API_TIMEOUT = values.PositiveIntegerValue(
1, environ_name="BREVO_API_TIMEOUT", environ_prefix=None
)
# Lobby configurations
LOBBY_KEY_PREFIX = values.Value(
@@ -525,6 +533,13 @@ class Base(Configuration):
environ_prefix=None,
)
# Calendar integrations
ROOM_CREATION_CALLBACK_CACHE_TIMEOUT = values.PositiveIntegerValue(
600, # 10 minutes
environ_name="ROOM_CREATION_CALLBACK_CACHE_TIMEOUT",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
@@ -576,6 +591,9 @@ class Base(Configuration):
scope = sentry_sdk.get_global_scope()
scope.set_tag("application", "backend")
# Ignore the logs added by the DockerflowMiddleware
ignore_logger("request.summary")
class Build(Base):
"""Settings used when the application is built.
+12 -13
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.15"
version = "0.1.17"
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.37.4",
"boto3==1.37.18",
"Brotli==1.1.0",
"brevo-python==1.1.2",
"celery[redis]==5.4.0",
@@ -37,7 +37,7 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.5",
"django-timezone-field>=5.1",
"django==5.1.7",
"django==5.1.8",
"djangorestframework==3.15.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
@@ -48,17 +48,16 @@ dependencies = [
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.5",
"psycopg[binary]==3.2.6",
"PyJWT==2.10.1",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.22.0",
"sentry-sdk==2.24.1",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
"livekit-api==0.8.2",
"aiohttp==3.11.13",
"aiohttp==3.11.14",
]
[project.urls]
@@ -73,18 +72,18 @@ dev = [
"drf-spectacular-sidecar==2025.3.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==9.0.1",
"pyfakefs==5.7.4",
"ipython==9.0.2",
"pyfakefs==5.8.0",
"pylint-django==2.6.1",
"pylint==3.3.4",
"pylint==3.3.6",
"pytest-cov==6.0.0",
"pytest-django==4.10.0",
"pytest==8.3.5",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.6",
"ruff==0.9.9",
"types-requests==2.32.0.20250301",
"responses==0.25.7",
"ruff==0.11.2",
"types-requests==2.32.0.20250306",
]
[tool.setuptools]
+1 -1
View File
@@ -38,7 +38,7 @@ RUN npm run build
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2
USER nginx
+1274 -2069
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.15",
"version": "0.1.17",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -16,45 +16,45 @@
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.4",
"@livekit/track-processors": "0.3.3",
"@pandacss/preset-panda": "0.53.0",
"@react-aria/toast": "3.0.0-beta.19",
"@pandacss/preset-panda": "0.53.3",
"@react-aria/toast": "3.0.1",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.67.1",
"@tanstack/react-query": "5.69.0",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.3",
"i18next": "24.2.2",
"i18next": "24.2.3",
"i18next-browser-languagedetector": "8.0.4",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.9.5",
"posthog-js": "1.225.1",
"livekit-client": "2.9.8",
"posthog-js": "1.232.6",
"react": "18.3.1",
"react-aria-components": "1.6.0",
"react-aria-components": "1.7.1",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"use-sound": "5.0.0",
"valtio": "2.1.3",
"valtio": "2.1.4",
"wouter": "3.6.0"
},
"devDependencies": {
"@pandacss/dev": "0.53.0",
"@tanstack/eslint-plugin-query": "5.66.1",
"@tanstack/react-query-devtools": "5.67.1",
"@types/node": "22.13.9",
"@pandacss/dev": "0.53.3",
"@tanstack/eslint-plugin-query": "5.68.0",
"@tanstack/react-query-devtools": "5.69.0",
"@types/node": "22.13.13",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@typescript-eslint/eslint-plugin": "8.26.0",
"@typescript-eslint/parser": "8.26.0",
"@typescript-eslint/eslint-plugin": "8.28.0",
"@typescript-eslint/parser": "8.28.0",
"@vitejs/plugin-react": "4.3.4",
"eslint": "8.57.0",
"eslint-config-prettier": "10.0.2",
"eslint-config-prettier": "10.1.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.19",
"postcss": "8.5.3",
"prettier": "3.5.3",
"typescript": "5.8.2",
"vite": "6.2.0",
"vite": "6.2.4",
"vite-tsconfig-paths": "5.1.4"
}
}
+2 -34
View File
@@ -35,40 +35,7 @@ const config: Config = {
exclude: [],
jsxFramework: 'react',
outdir: 'src/styled-system',
globalFontface: {
Marianne: [
{
src: 'url(/fonts/Marianne-Regular-subset.woff2) format("woff2")',
fontWeight: 400,
fontStyle: 'normal',
fontDisplay: 'swap',
},
{
path: 'url(/fonts/Marianne-Regular_Italic-subset.woff2) format("woff2")',
fontWeight: 400,
fontStyle: 'italic',
fontDisplay: 'swap',
},
{
path: 'url(/fonts/Marianne-Medium-subset.woff2) format("woff2")',
fontWeight: 500,
fontStyle: 'normal',
fontDisplay: 'swap',
},
{
path: 'url(/fonts/Marianne-Bold-subset.woff2) format("woff2")',
fontWeight: 700,
fontStyle: 'normal',
fontDisplay: 'swap',
},
{
path: 'url(/fonts/Marianne-ExtraBold-subset.woff2) format("woff2")',
fontWeight: 800,
fontStyle: 'normal',
fontDisplay: 'swap',
},
],
},
globalFontface: {},
theme: {
...pandaPreset.theme,
// media queries are defined in em so that zooming with text-only mode triggers breakpoints
@@ -223,6 +190,7 @@ const config: Config = {
fonts: {
sans: {
value: [
'Marianne',
'Source Sans',
'Source Sans fallback',
'ui-sans-serif',
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+5 -26
View File
@@ -5,7 +5,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClientProvider } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useLang } from 'hoofd'
import { Switch, Route, useLocation } from 'wouter'
import { Switch, Route } from 'wouter'
import { I18nProvider } from 'react-aria-components'
import { Layout } from './layout/Layout'
import { NotFoundScreen } from './components/NotFoundScreen'
@@ -13,35 +13,11 @@ import { routes } from './routes'
import './i18n/init'
import { queryClient } from '@/api/queryClient'
import { AppInitialization } from '@/components/AppInitialization'
import { SdkCreateButton } from './features/sdk/routes/CreateButton'
const SDK_BASE_ROUTE = '/sdk'
function App() {
const { i18n } = useTranslation()
useLang(i18n.language)
const [location] = useLocation()
const isSDKRoute = location.startsWith(SDK_BASE_ROUTE)
if (isSDKRoute) {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={null}>
<I18nProvider locale={i18n.language}>
<Switch>
<Route path={SDK_BASE_ROUTE} nest>
<Route path="/create-button">
<SdkCreateButton />
</Route>
</Route>
</Switch>
</I18nProvider>
</Suspense>
</QueryClientProvider>
)
}
return (
<QueryClientProvider client={queryClient}>
<AppInitialization />
@@ -55,7 +31,10 @@ function App() {
<Route component={NotFoundScreen} />
</Switch>
</Layout>
<ReactQueryDevtools initialIsOpen={false} buttonPosition="top-left" />
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-left"
/>
</I18nProvider>
</Suspense>
</QueryClientProvider>
+1
View File
@@ -4,4 +4,5 @@ export const keys = {
config: 'config',
requestEntry: 'requestEntry',
waitingParticipants: 'waitingParticipants',
roomCreationCallback: 'roomCreationCallback',
}
@@ -2,9 +2,13 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
import { useLocation } from 'wouter'
const SDK_BASE_ROUTE = '/sdk'
export const AppInitialization = () => {
const { data } = useConfig()
const [location] = useLocation()
const {
analytics = {},
@@ -12,8 +16,11 @@ export const AppInitialization = () => {
silence_livekit_debug_logs = false,
} = data || {}
useAnalytics(analytics)
useSupport(support)
const isSDKContext = location.includes(SDK_BASE_ROUTE)
useAnalytics({ ...analytics, isDisabled: isSDKContext })
useSupport({ ...support, isDisabled: isSDKContext })
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
+10 -3
View File
@@ -1,4 +1,4 @@
import { cva, RecipeVariantProps } from '@/styled-system/css'
import { css, cva, RecipeVariantProps } from '@/styled-system/css'
import React from 'react'
const avatar = cva({
@@ -19,7 +19,8 @@ const avatar = cva({
list: {
width: '32px',
height: '32px',
fontSize: '1.25rem',
fontSize: '1.3rem',
lineHeight: '1rem',
},
placeholder: {
width: '100%',
@@ -60,7 +61,13 @@ export const Avatar = ({
className={avatar({ context, notification })}
{...props}
>
{initial}
<span
className={css({
marginTop: '-0.3rem',
})}
>
{initial}
</span>
</div>
)
}
@@ -25,7 +25,9 @@ export const FeedbackBanner = () => {
})}
>
<RiErrorWarningLine size={20} aria-hidden="true" />
<Text as="p">{t('feedback.context')}</Text>
<Text as="p" variant="sm">
{t('feedback.context')}
</Text>
<div
className={css({
display: 'flex',
@@ -33,7 +35,7 @@ export const FeedbackBanner = () => {
gap: 0.25,
})}
>
<A href={GRIST_FORM} target="_blank">
<A href={GRIST_FORM} target="_blank" size="sm">
{t('feedback.cta')}
</A>
<RiExternalLinkLine size={16} aria-hidden="true" />
@@ -17,18 +17,19 @@ export const terminateAnalyticsSession = () => {
export type useAnalyticsProps = {
id?: string
host?: string
isDisabled?: boolean
}
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => {
const [location] = useLocation()
useEffect(() => {
if (!id || !host) return
if (!id || !host || isDisabled) return
if (posthog.__loaded) return
posthog.init(id, {
api_host: host,
person_profiles: 'always',
})
}, [id, host])
}, [id, host, isDisabled])
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
useEffect(() => {
@@ -48,7 +48,7 @@ export const useUser = (
const isLoggedOut = isLoggedIn === false
return {
...query,
refetch: query.refetch,
user: isLoggedOut ? undefined : (query.data as ApiUser | undefined),
isLoggedIn,
logout,
@@ -17,12 +17,13 @@ const Heading = styled('h2', {
base: {
width: 'fit-content',
marginBottom: 0,
fontSize: '1.5rem',
fontSize: '1.3rem',
fontWeight: '700',
marginTop: '0.75rem',
lineHeight: '2rem',
lineHeight: '1.7rem',
maxWidth: '23rem',
textAlign: 'center',
textWrap: 'pretty',
textWrap: 'balance',
},
})
@@ -31,7 +32,7 @@ const Body = styled('p', {
maxWidth: '23rem',
textAlign: 'center',
textWrap: 'pretty',
lineHeight: '1.2rem',
lineHeight: '1.4rem',
fontSize: '1rem',
},
})
@@ -102,7 +103,7 @@ const Slide = styled('div', {
alignItems: 'center',
gap: '0.5rem',
justifyContent: 'start',
minHeight: { base: 'none', xsm: '550px' },
minHeight: { base: 'none', xsm: '580px' },
minWidth: { base: 'none', xsm: '200px' },
width: { base: '100%', xsm: '22.625rem' },
},
+9 -23
View File
@@ -13,12 +13,11 @@ import { RiAddLine, RiLink } from '@remixicon/react'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { IntroSlider } from '@/features/home/components/IntroSlider'
import { MoreLink } from '@/features/home/components/MoreLink'
import { ReactNode, useEffect, useState } from 'react'
import { ReactNode, useState } from 'react'
import { css } from '@/styled-system/css'
import { menuRecipe } from '@/primitives/menuRecipe.ts'
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
import { SdkReverseClient } from '@/features/sdk/SdkReverseClient'
const Columns = ({ children }: { children?: ReactNode }) => {
return (
@@ -72,8 +71,8 @@ const LeftColumn = ({ children }: { children?: ReactNode }) => {
textAlign: 'left',
alignItems: 'flex-start',
flexShrink: '1',
flexBasis: '36rem',
maxWidth: '36rem',
flexBasis: '40rem',
maxWidth: '40rem',
padding: '1em 3em',
},
})}
@@ -120,14 +119,13 @@ const Separator = styled('div', {
const Heading = styled('h1', {
base: {
fontWeight: '500',
fontWeight: '700',
fontStyle: 'normal',
fontStretch: 'normal',
fontOpticalSizing: 'auto',
marginBottom: 0,
paddingBottom: '0.75rem',
paddingBottom: '1.2rem',
fontSize: '2.3rem',
lineHeight: '2.5rem',
lineHeight: '2.6rem',
letterSpacing: '0',
xsm: {
fontSize: '3rem',
@@ -139,9 +137,9 @@ const Heading = styled('h1', {
const IntroText = styled('div', {
base: {
marginBottom: '3rem',
fontSize: '1.5rem',
lineHeight: '1.8rem',
textWrap: 'pretty',
fontSize: '1.2rem',
lineHeight: '1.5rem',
textWrap: 'balance',
maxWidth: '32rem',
},
})
@@ -157,18 +155,6 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
const { user } = useUser()
/**
* Used for SDK popup to close automatically.
*/
useEffect(() => {
if (!user) {
return
}
SdkReverseClient.broadcastAuthentication()
}, [user])
return (
<UserAware>
<Screen>
@@ -1,38 +1,42 @@
import { Screen } from '@/layout/Screen'
import { Bold, H, P, A, Italic, Ul } from '@/primitives'
import { css } from '@/styled-system/css'
import { H, P, A, Italic, Ul } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
export const AccessibilityRoute = () => {
const { t } = useTranslation('accessibility', { keyPrefix: 'accessibility' })
const indentedStyle = css({
paddingLeft: '1.5rem',
marginLeft: '1rem',
borderLeft: '1px solid black',
marginTop: '1.5rem',
})
return (
<Screen layout="centered" headerTitle={t('title')}>
<HStack display={'block'} padding={'2rem'}>
<P dangerouslySetInnerHTML={{ __html: t('introduction') }}></P>
<H lvl={2} bold>
{t('declaration.title')}
</H>
<Italic>{t('declaration.date')}</Italic>
<P>{t('scope')}</P>
<P>
{t('scope').replace('visio.numerique.gouv.fr', '')}{' '}
<A href="https://visio.numerique.gouv.fr" color="primary">
visio.numerique.gouv.fr
</A>
.
</P>
<H lvl={2} bold>
{t('complianceStatus.title')}
</H>
<P>{t('complianceStatus.body')}</P>
<P>
<A href="https://visio.numerique.gouv.fr" color="primary">
visio.numerique.gouv.fr
</A>
{t('complianceStatus.body').replace('visio.numerique.gouv.fr', '')}
</P>
<H lvl={2} bold>
{t('improvement.title')}
</H>
<P>{t('improvement.body')}</P>
<Ul
style={{
marginBottom: '1rem',
@@ -43,12 +47,13 @@ export const AccessibilityRoute = () => {
'visio@numerique.gouv.fr',
''
)}
<A href="mailto:visio@numerique.gouv.fr" color="primary">
visio@numerique.gouv.fr
<A href="mailto:lasuite@modernisation.gouv.fr" color="primary">
lasuite@modernisation.gouv.fr
</A>
</li>
<li>{t('improvement.contact.address')}</li>
</Ul>
<P>{t('improvement.response')}</P>
<H lvl={2} bold>
@@ -62,16 +67,11 @@ export const AccessibilityRoute = () => {
<li>{t('recourse.options.option1')}</li>
<li>{t('recourse.options.option2')}</li>
<li>
{t('recourse.options.option3')}
<P className={indentedStyle}>
<Bold>{t('dataProtection.line1')}</Bold>
<br />
{t('dataProtection.line2')}
<br />
{t('dataProtection.line3')}
<br />
{t('dataProtection.line4')}
</P>
<P
dangerouslySetInnerHTML={{
__html: t('recourse.options.option3'),
}}
></P>
</li>
</Ul>
</HStack>
@@ -1,5 +1,5 @@
import { Screen } from '@/layout/Screen'
import { Bold, H, P, A } from '@/primitives'
import { Bold, H, P, A, Link } from '@/primitives'
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
@@ -17,123 +17,57 @@ export const LegalTermsRoute = () => {
return (
<Screen layout="centered" headerTitle={t('title')}>
<HStack display={'block'} padding={'2rem'}>
{/* Éditeur section */}
<H lvl={2}>{t('creator.title')}</H>
<P>{t('creator.body')}</P>
<H lvl={3} bold>
{t('creator.contact.title')}
</H>
<P>
{t('creator.contact.address', {
address: 'DINUM, 20 avenue de Ségur, 75007 Paris',
})}
<P className={indentedStyle}>
{t('creator.contact.address')}
<br />
{t('creator.contact.phone', {
phone: '01.71.21.01.70',
})}
{t('creator.contact.city')}
<br />
{t('creator.contact.siret', {
siret: '12000101100010 (secrétariat général du gouvernement)',
})}
{t('creator.contact.phone')}
<br />
{t('creator.contact.siren', {
siren: '120 001 011',
})}
{t('creator.contact.siret')}
<br />
{t('creator.contact.siren')}
</P>
{/* Directeur de la publication section */}
<H lvl={2}>{t('director.title')}</H>
<P>{t('director.body')}</P>
{/* Hébergement section */}
<H lvl={2}>{t('hosting.title')}</H>
<P>{t('hosting.body')}</P>
<P className={indentedStyle}>
<Bold>{t('hosting.address.line1')}</Bold>
<br />
{t('hosting.address.line2')}
<br />
{t('hosting.address.line3')}
<br />
{t('hosting.address.line4')}
</P>
{/* Accessibilité section */}
<H lvl={2}>{t('accessibility.title')}</H>
<P>{t('accessibility.body')}</P>
<P>
{t('accessibility.more')}
<Link to="/accessibilite" color="primary">
{t('accessibility.link')}
</Link>
</P>
<P>
<Bold>{t('accessibility.status')}</Bold>
</P>
<H lvl={3} bold>
{t('accessibility.reporting.title')}
</H>
<P>
{t('accessibility.reporting.body1').replace(
'visio@numerique.gouv.fr.',
''
)}
<A href="mailto:visio@numerique.gouv.fr" color="primary">
visio@numerique.gouv.fr
</A>
.
</P>
<P>
{t('accessibility.reporting.body2').replace(
'https://formulaire.defenseurdesdroits.fr',
''
)}{' '}
<A href="https://formulaire.defenseurdesdroits.fr" color="primary">
https://formulaire.defenseurdesdroits.fr
</A>{' '}
{
t('accessibility.reporting.body2').split(
'https://formulaire.defenseurdesdroits.fr'
)[1]
}
</P>
<P className={indentedStyle}>
<Bold>{t('accessibility.reporting.address.line1')}</Bold>
<br />
{t('accessibility.reporting.address.line2')}
<br />
{t('accessibility.reporting.address.line3')}
</P>
{/* Content reuse section */}
{/* Réutilisation des contenus et liens section */}
<H lvl={2}>{t('reuse.title')}</H>
<P>{t('reuse.body1')}</P>
<P>{t('reuse.body2')}</P>
{/* Media subsection */}
<H lvl={3} bold>
{t('reuse.media.title')}
</H>
<P>
{t('reuse.media.body').replace('visio@numerique.gouv.fr.', '')}
<A href="mailto:visio@numerique.gouv.fr" color="primary">
visio@numerique.gouv.fr
{t('reuse.body1')}{' '}
<A
href="https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf"
color="primary"
>
{t('reuse.license')}
</A>
.
</P>
<H lvl={2}>{t('liability.title')}</H>
<P>{t('liability.body1')}</P>
<P>{t('liability.body2')}</P>
<P>{t('liability.body3')}</P>
<P>{t('liability.body4')}</P>
<H lvl={2}>{t('dataProtection.title')}</H>
<P>
{t('dataProtection.body').replace('dpd@pm.gouv.fr', '')}{' '}
<A href="mailto:dpd@pm.gouv.fr" color="primary">
dpd@pm.gouv.fr
</A>{' '}
{t('dataProtection.body').split('dpd@pm.gouv.fr')[1]}
</P>
<P className={indentedStyle}>
<Bold>{t('dataProtection.address.line1')}</Bold>
<br />
{t('dataProtection.address.line2')}
<br />
{t('dataProtection.address.line3')}
<br />
{t('dataProtection.address.line4')}
</P>
<P>{t('reuse.body2')}</P>
<P>{t('reuse.body3')}</P>
</HStack>
</Screen>
)
@@ -1,6 +1,5 @@
import { Screen } from '@/layout/Screen'
import { Bold, H, P, A, Ul } from '@/primitives'
import { css } from '@/styled-system/css'
import { H, P, A, Ul } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
@@ -17,52 +16,40 @@ export const TermsOfServiceRoute = () => {
const { t } = useTranslation('termsOfService')
return (
<Screen layout="centered" headerTitle={t('terms.title')}>
<Screen layout="centered" headerTitle={t('title')}>
<HStack display={'block'} padding={'2rem'}>
<P>{t('terms.introduction')}</P>
{/* Article 1 */}
<H lvl={2}>{t('terms.articles.article1.title')}</H>
<P>{t('terms.articles.article1.content')}</P>
<H lvl={2}>{t('articles.article1.title')}</H>
<P>{t('articles.article1.content')}</P>
{/* Article 2 */}
<H lvl={2}>{t('terms.articles.article2.title')}</H>
<P>{t('terms.articles.article2.content')}</P>
<Ul>
{ensureArray(
t('terms.articles.article2.purposes', { returnObjects: true })
).map((purpose, index) => (
<li key={index}>{purpose}</li>
))}
</Ul>
<H lvl={2}>{t('articles.article2.title')}</H>
<P>{t('articles.article2.content')}</P>
<P>{t('articles.article2.purposes')}</P>
{/* Article 3 */}
<H lvl={2}>{t('terms.articles.article3.title')}</H>
{ensureArray(
t('terms.articles.article3.definitions', { returnObjects: true })
).map((def, index) => (
<P key={index}>
<Bold>{`"${def.term}"`}</Bold> {def.definition}
</P>
))}
<H lvl={2}>{t('articles.article3.title')}</H>
<P>{t('articles.article3.definition')}</P>
{/* Article 4 */}
<H lvl={2}>{t('terms.articles.article4.title')}</H>
<P>{t('terms.articles.article4.content')}</P>
<H lvl={2}>{t('articles.article4.title')}</H>
<P>{t('articles.article4.content')}</P>
{/* Article 5 */}
<H lvl={2}>{t('terms.articles.article5.title')}</H>
<H lvl={2} margin={false}>
{t('articles.article5.title')}
</H>
{/* Section 5.1 */}
<H lvl={3} bold>
{t('terms.articles.article5.sections.section1.title')}
{t('articles.article5.sections.section1.title')}
</H>
<P>{t('terms.articles.article5.sections.section1.content')}</P>
<P>{t('terms.articles.article5.sections.section1.paragraph1')}</P>
<P>{t('terms.articles.article5.sections.section1.paragraph2')}</P>
<P>{t('articles.article5.sections.section1.content')}</P>
<P>{t('articles.article5.sections.section1.paragraph1')}</P>
<P>{t('articles.article5.sections.section1.paragraph2')}</P>
<Ul>
{ensureArray(
t('terms.articles.article5.sections.section1.capabilities', {
t('articles.article5.sections.section1.capabilities', {
returnObjects: true,
})
).map((capability, index) => (
@@ -70,22 +57,23 @@ export const TermsOfServiceRoute = () => {
))}
</Ul>
<P
style={{
marginTop: '1.5rem',
style={{ marginTop: '0.75rem' }}
dangerouslySetInnerHTML={{
__html: t('articles.article5.sections.section1.paragraph3'),
}}
>
{t('terms.articles.article5.sections.section1.paragraph3')}
</P>
></P>
{/* Section 5.2 */}
<H lvl={3} bold>
{t('terms.articles.article5.sections.section2.title')}
{t('articles.article5.sections.section2.title')}
</H>
<P>{t('terms.articles.article5.sections.section2.content')}</P>
<P>{t('terms.articles.article5.sections.section2.paragraph')}</P>
<P>{t('articles.article5.sections.section2.content')}</P>
<P style={{ marginTop: '0.75rem' }}>
{t('articles.article5.sections.section2.paragraph')}
</P>
<Ul>
{ensureArray(
t('terms.articles.article5.sections.section2.capabilities', {
t('articles.article5.sections.section2.capabilities', {
returnObjects: true,
})
).map((capability, index) => (
@@ -94,14 +82,16 @@ export const TermsOfServiceRoute = () => {
</Ul>
{/* Article 6 */}
<H lvl={2}>{t('terms.articles.article6.title')}</H>
<H lvl={2} margin={false}>
{t('articles.article6.title')}
</H>
{/* Section 6.1 */}
<H lvl={3} bold>
{t('terms.articles.article6.sections.section1.title')}
{t('articles.article6.sections.section1.title')}
</H>
{ensureArray(
t('terms.articles.article6.sections.section1.paragraphs', {
t('articles.article6.sections.section1.paragraphs', {
returnObjects: true,
})
).map((paragraph, index) => (
@@ -110,10 +100,10 @@ export const TermsOfServiceRoute = () => {
{/* Section 6.2 */}
<H lvl={3} bold>
{t('terms.articles.article6.sections.section2.title')}
{t('articles.article6.sections.section2.title')}
</H>
{ensureArray(
t('terms.articles.article6.sections.section2.paragraphs', {
t('articles.article6.sections.section2.paragraphs', {
returnObjects: true,
})
).map((paragraph, index) => (
@@ -121,250 +111,45 @@ export const TermsOfServiceRoute = () => {
))}
{/* Article 7 */}
<H lvl={2}>{t('terms.articles.article7.title')}</H>
<H lvl={2} margin={false}>
{t('articles.article7.title')}
</H>
{/* Section 7.1 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section1.title')}
{t('articles.article7.sections.section1.title')}
</H>
<P>{t('terms.articles.article7.sections.section1.content')}</P>
{/* Section 7.2 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section2.title')}
</H>
<P>{t('terms.articles.article7.sections.section2.content')}</P>
<P>{t('articles.article7.sections.section1.content')}</P>
{ensureArray(
t('terms.articles.article7.sections.section2.dataCategories', {
returnObjects: true,
})
).map((category, catIndex) => (
<div key={catIndex}>
<P>
<Bold>{category.category}</Bold>
</P>
{category.items && (
<Ul>
{ensureArray(category.items).map((item, itemIndex) => (
<li key={itemIndex}>{item}</li>
))}
</Ul>
)}
</div>
))}
{/* Section 7.3 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section3.title')}
</H>
<P>{t('terms.articles.article7.sections.section3.content')}</P>
<Ul>
{ensureArray(
t('terms.articles.article7.sections.section3.purposes', {
returnObjects: true,
})
).map((purpose, index) => (
<li key={index}>{purpose}</li>
))}
</Ul>
{/* Section 7.4 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section4.title')}
</H>
<P>{t('terms.articles.article7.sections.section4.content')}</P>
<Ul>
{ensureArray(
t('terms.articles.article7.sections.section4.bases', {
returnObjects: true,
})
).map((basis, index) => (
<li key={index}>{basis}</li>
))}
</Ul>
{ensureArray(
t('terms.articles.article7.sections.section4.details', {
returnObjects: true,
})
).map((detail, detailIndex) => (
<div key={detailIndex}>
<P>
<Bold>{detail.title}</Bold>
</P>
<P>{detail.content}</P>
<P>{detail.legalReference}</P>
{detail.legalReferences && (
<Ul>
{ensureArray(detail.legalReferences).map((ref, refIndex) => (
<li key={refIndex}>{ref}</li>
))}
</Ul>
)}
</div>
))}
{/* Section 7.5 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section5.title')}
</H>
<P>{t('terms.articles.article7.sections.section5.content')}</P>
<table
className={css({
width: '100%',
borderCollapse: 'collapse',
border: '1px solid #ddd',
marginBottom: '1rem',
})}
>
<thead>
<tr>
<th
className={css({
padding: '0.5rem',
border: '1px solid #ddd',
textAlign: 'left',
})}
>
{t('terms.articles.article7.sections.section5.dataType')}
</th>
<th
className={css({
padding: '0.5rem',
border: '1px solid #ddd',
textAlign: 'left',
})}
>
{t('terms.articles.article7.sections.section5.retentionPeriod')}
</th>
</tr>
</thead>
<tbody>
{ensureArray(
t('terms.articles.article7.sections.section5.retentionTable', {
returnObjects: true,
})
).map((row, rowIndex) => (
<tr key={rowIndex}>
<td
className={css({
padding: '0.5rem',
border: '1px solid #ddd',
})}
>
{row.dataType}
</td>
<td
className={css({
padding: '0.5rem',
border: '1px solid #ddd',
})}
>
{row.retention}
</td>
</tr>
))}
</tbody>
</table>
<P>{t('terms.articles.article7.sections.section5.additionalInfo')}</P>
{/* Section 7.6 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section6.title')}
</H>
<P>{t('terms.articles.article7.sections.section6.content')}</P>
{/* Section 7.7 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section7.title')}
</H>
<P>{t('terms.articles.article7.sections.section7.content')}</P>
<Ul
style={{
marginBottom: '1rem',
}}
>
{ensureArray(
t('terms.articles.article7.sections.section7.rights', {
returnObjects: true,
})
).map((right, index) => (
<li key={index}>{right}</li>
))}
</Ul>
<P>
{t(
'terms.articles.article7.sections.section7.exerciseRights.content'
)}
</P>
<Ul
style={{
marginBottom: '1rem',
}}
>
{ensureArray(
t(
'terms.articles.article7.sections.section7.exerciseRights.methods',
{ returnObjects: true }
)
).map((method, index) => {
if (
typeof method === 'string' &&
method.includes('visio@numerique.gouv.fr')
) {
const parts = method.split('visio@numerique.gouv.fr')
return (
<li key={index}>
{parts[0]}
<A href="mailto:visio@numerique.gouv.fr" color="primary">
visio@numerique.gouv.fr
</A>
{parts[1]}
</li>
)
}
return <li key={index}>{method}</li>
})}
</Ul>
{ensureArray(
t('terms.articles.article7.sections.section7.additionalInfo', {
returnObjects: true,
})
).map((info, index) => {
if (typeof info === 'string' && info.includes('Cnil')) {
const parts = info.split('Cnil')
return (
<P key={index}>
{parts[0]}
<A
href="https://www.cnil.fr/fr/modele/courrier/sopposer-au-traitement-de-donnees"
color="primary"
>
Cnil
</A>
{parts[1]}
</P>
)
}
return <P key={index}>{info}</P>
})}
{/* Section 7.8 */}
<H lvl={3} bold>
{t('terms.articles.article7.sections.section8.title')}
</H>
{ensureArray(
t('terms.articles.article7.sections.section8.paragraphs', {
t('articles.article7.sections.section1.paragraphs', {
returnObjects: true,
})
).map((paragraph, index) => (
<P key={index}>{paragraph}</P>
))}
{/* Section 7.2 */}
<H lvl={3} bold>
{t('articles.article7.sections.section2.title')}
</H>
<P>
{t('articles.article7.sections.section2.content')
.split('https://github.com/suitenumerique/meet')[0]
.replace('https://github.com/suitenumerique/meet', '')}{' '}
<A href="https://github.com/suitenumerique/meet" color="primary">
https://github.com/suitenumerique/meet
</A>
{'. '}
{
t('articles.article7.sections.section2.content').split(
'https://github.com/suitenumerique/meet'
)[1]
}
</P>
{/* Article 8 */}
<H lvl={2}>{t('articles.article8.title')}</H>
<P>{t('articles.article8.content')}</P>
</HStack>
</Screen>
)
@@ -5,17 +5,20 @@ import { ApiRoom } from './ApiRoom'
export interface CreateRoomParams {
slug: string
callbackId?: string
username?: string
}
const createRoom = ({
slug,
callbackId,
username = '',
}: CreateRoomParams): Promise<ApiRoom> => {
return fetchApi(`rooms/?username=${encodeURIComponent(username)}`, {
method: 'POST',
body: JSON.stringify({
name: slug,
callback_id: callbackId,
}),
})
}
@@ -298,7 +298,7 @@ export const Join = ({
case ApiLobbyStatus.TIMEOUT:
return (
<VStack alignItems="center" textAlign="center">
<H lvl={1} margin={false}>
<H lvl={1} margin={false} centered>
{t('timeoutInvite.title')}
</H>
<Text as="p" variant="note">
@@ -310,7 +310,7 @@ export const Join = ({
case ApiLobbyStatus.DENIED:
return (
<VStack alignItems="center" textAlign="center">
<H lvl={1} margin={false}>
<H lvl={1} margin={false} centered>
{t('denied.title')}
</H>
<Text as="p" variant="note">
@@ -322,7 +322,7 @@ export const Join = ({
case ApiLobbyStatus.WAITING:
return (
<VStack alignItems="center" textAlign="center">
<H lvl={1} margin={false}>
<H lvl={1} margin={false} centered>
{t('waiting.title')}
</H>
<Text
@@ -346,7 +346,7 @@ export const Join = ({
}}
>
<VStack marginBottom={1}>
<H lvl={1} margin="sm">
<H lvl={1} margin="sm" centered>
{t('heading')}
</H>
<Field
@@ -37,7 +37,14 @@ export const Admin = () => {
flexDirection="column"
alignItems="start"
>
<Text variant="note" wrap="pretty" margin="md">
<Text
variant="note"
wrap="pretty"
className={css({
textStyle: 'sm',
})}
margin={'md'}
>
{t('description')}
</Text>
<RACSeparator
@@ -68,8 +75,8 @@ export const Admin = () => {
</Text>
<Field
type="radioGroup"
label="Type d'accès à la réunion"
aria-label="Type d'accès à la réunion"
label={t('access.type')}
aria-label={t('access.type')}
labelProps={{
className: css({
fontSize: '1rem',
@@ -75,7 +75,7 @@ export function FloatingReaction({
>
<span
className={css({
lineHeight: '45px',
lineHeight: '57px',
})}
>
{emoji}
@@ -89,9 +89,10 @@ export function FloatingReaction({
textAlign: 'center',
borderRadius: '20px',
paddingX: '0.5rem',
paddingY: '0.15rem',
paddingBottom: '0.3125rem',
paddingTop: '0.15rem',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
lineHeight: '14px',
lineHeight: '16px',
})}
>
{name}
@@ -0,0 +1,66 @@
import { A, Button, Dialog, P } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css'
// todo - refactor it into a generic system
export const ScreenShareErrorModal = ({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' })
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1
return (
<Dialog
isOpen={isOpen}
role="alertdialog"
title={t('title')}
aria-label={t('ariaLabel')}
onClose={onClose}
>
{({ close }) => {
return (
<>
<P>
{t('message')}{' '}
{isMac && (
<>
{t('macInstructions')}{' '}
<A
href="x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
color="primary"
aria-label={t('macSystemPreferences') + '-' + t('newTab')}
>
{t('macSystemPreferences')}
</A>
.{' '}
</>
)}
{t('helpLinkText')}{' '}
<A
href="https://lasuite.crisp.help/fr/article/visio-probleme-de-presentation-1xkf799/"
aria-label={t('helpLinkLabel') + '-' + t('newTab')}
target="_blank"
color="primary"
>
{t('helpLinkLabel')}
</A>
.
</P>
<Button
onPress={close}
size="sm"
variant="primary"
className={css({ marginLeft: 'auto', marginTop: '2rem' })}
>
{t('closeButton')}
</Button>
</>
)
}}
</Dialog>
)
}
@@ -13,7 +13,7 @@ import {
/**
* This is simply a wrapper around track-processor-js Processor
* in order to be compatible with a common interface BackgroundBlurProcessorInterface
* used accross the project.
* used across the project.
*/
export class BackgroundBlurTrackProcessorJsWrapper
implements BackgroundProcessorInterface
@@ -0,0 +1,345 @@
import { ProcessorOptions, Track } from 'livekit-client'
import posthog from 'posthog-js'
import {
FilesetResolver,
FaceLandmarker,
FaceLandmarkerResult,
} from '@mediapipe/tasks-vision'
import {
CLEAR_TIMEOUT,
SET_TIMEOUT,
TIMEOUT_TICK,
timerWorkerScript,
} from './TimerWorker'
import {
BackgroundProcessorInterface,
BackgroundOptions,
ProcessorType,
} from '.'
const PROCESSING_WIDTH = 256 * 3
const PROCESSING_HEIGHT = 144 * 3
const FACE_LANDMARKS_CANVAS_ID = 'face-landmarks-local'
export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
options: BackgroundOptions
name: string
processedTrack?: MediaStreamTrack | undefined
source?: MediaStreamTrack
sourceSettings?: MediaTrackSettings
videoElement?: HTMLVideoElement
videoElementLoaded?: boolean
// Canvas containing the video processing result
outputCanvas?: HTMLCanvasElement
outputCanvasCtx?: CanvasRenderingContext2D
faceLandmarker?: FaceLandmarker
faceLandmarkerResult?: FaceLandmarkerResult
// The resized image of the video source
sourceImageData?: ImageData
timerWorker?: Worker
type: ProcessorType
// Effect images
glassesImage?: HTMLImageElement
mustacheImage?: HTMLImageElement
beretImage?: HTMLImageElement
constructor(opts: BackgroundOptions) {
this.name = 'face_landmarks'
this.options = opts
this.type = ProcessorType.FACE_LANDMARKS
this._initEffectImages()
}
private _initEffectImages() {
this.glassesImage = new Image()
this.glassesImage.src = '/assets/glasses.png'
this.glassesImage.crossOrigin = 'anonymous'
this.mustacheImage = new Image()
this.mustacheImage.src = '/assets/mustache.png'
this.mustacheImage.crossOrigin = 'anonymous'
this.beretImage = new Image()
this.beretImage.src = '/assets/beret.png'
this.beretImage.crossOrigin = 'anonymous'
}
static get isSupported() {
return true // Face landmarks should work in all modern browsers
}
async init(opts: ProcessorOptions<Track.Kind>) {
if (!opts.element) {
throw new Error('Element is required for processing')
}
this.source = opts.track as MediaStreamTrack
this.sourceSettings = this.source!.getSettings()
this.videoElement = opts.element as HTMLVideoElement
this._createMainCanvas()
const stream = this.outputCanvas!.captureStream()
const tracks = stream.getVideoTracks()
if (tracks.length == 0) {
throw new Error('No tracks found for processing')
}
this.processedTrack = tracks[0]
await this.initFaceLandmarker()
this._initWorker()
posthog.capture('face-landmarks-init')
}
_initWorker() {
this.timerWorker = new Worker(timerWorkerScript, {
name: 'FaceLandmarks',
})
this.timerWorker.onmessage = (data) => this.onTimerMessage(data)
if (this.videoElementLoaded) {
this.timerWorker!.postMessage({
id: SET_TIMEOUT,
timeMs: 1000 / 30,
})
} else {
this.videoElement!.onloadeddata = () => {
this.videoElementLoaded = true
this.timerWorker!.postMessage({
id: SET_TIMEOUT,
timeMs: 1000 / 30,
})
}
}
}
onTimerMessage(response: { data: { id: number } }) {
if (response.data.id === TIMEOUT_TICK) {
this.process()
}
}
async initFaceLandmarker() {
const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
)
this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath:
'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task',
delegate: 'GPU',
},
runningMode: 'VIDEO',
outputFaceBlendshapes: true,
outputFacialTransformationMatrixes: true,
})
}
async sizeSource() {
this.outputCanvasCtx?.drawImage(
this.videoElement!,
0,
0,
this.videoElement!.videoWidth,
this.videoElement!.videoHeight,
0,
0,
PROCESSING_WIDTH,
PROCESSING_HEIGHT
)
this.sourceImageData = this.outputCanvasCtx?.getImageData(
0,
0,
PROCESSING_WIDTH,
PROCESSING_HEIGHT
)
}
async detectFaces() {
const startTimeMs = performance.now()
this.faceLandmarkerResult = this.faceLandmarker!.detectForVideo(
this.sourceImageData!,
startTimeMs
)
}
private drawEffect(
leftPoint: { x: number; y: number },
rightPoint: { x: number; y: number },
image: HTMLImageElement,
widthScale: number,
heightScale: number,
yOffset: number = 0
) {
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(rightPoint.x - leftPoint.x, 2) +
Math.pow(rightPoint.y - leftPoint.y, 2)
)
// Scale image based on distance
const width = distance * PROCESSING_WIDTH * widthScale
const height = width * heightScale
// Calculate center position between points
const centerX = (leftPoint.x + rightPoint.x) / 2
const centerY = (leftPoint.y + rightPoint.y) / 2 + yOffset
// Draw image
this.outputCanvasCtx!.save()
this.outputCanvasCtx!.translate(
centerX * PROCESSING_WIDTH,
centerY * PROCESSING_HEIGHT
)
// Calculate rotation angle based on point positions
const angle = Math.atan2(
rightPoint.y - leftPoint.y,
rightPoint.x - leftPoint.x
)
this.outputCanvasCtx!.rotate(angle)
// Draw image centered at the midpoint between points
this.outputCanvasCtx!.drawImage(
image,
-width / 2,
-height / 2,
width,
height
)
this.outputCanvasCtx!.restore()
}
async drawFaceLandmarks() {
// Draw the original video frame at the canvas size
this.outputCanvasCtx!.drawImage(
this.videoElement!,
0,
0,
this.videoElement!.videoWidth,
this.videoElement!.videoHeight,
0,
0,
PROCESSING_WIDTH,
PROCESSING_HEIGHT
)
if (!this.faceLandmarkerResult?.faceLandmarks) {
return
}
// Draw face landmarks (optional, for debugging)
this.outputCanvasCtx!.strokeStyle = '#00FF00'
this.outputCanvasCtx!.lineWidth = 2
for (const face of this.faceLandmarkerResult.faceLandmarks) {
// Find eye landmarks
const leftEye = face[468]
const rightEye = face[473]
// Find mouth landmarks for mustache
const leftMoustache = face[92]
const rightMoustache = face[322]
// Find forehead landmarks for beret
const leftForehead = face[103]
const rightForehead = face[332]
if (leftEye && rightEye && this.options.showGlasses) {
this.drawEffect(leftEye, rightEye, this.glassesImage!, 2.5, 0.7)
}
if (leftMoustache && rightMoustache && this.options.showFrench) {
this.drawEffect(
leftMoustache,
rightMoustache,
this.mustacheImage!,
1.5,
0.5
)
}
if (leftForehead && rightForehead && this.options.showFrench) {
this.drawEffect(
leftForehead,
rightForehead,
this.beretImage!,
2.1,
0.7,
-0.1
)
}
}
}
async process() {
await this.sizeSource()
await this.detectFaces()
await this.drawFaceLandmarks()
this.timerWorker!.postMessage({
id: SET_TIMEOUT,
timeMs: 1000 / 30,
})
}
_createMainCanvas() {
this.outputCanvas = document.querySelector(
`#${FACE_LANDMARKS_CANVAS_ID}`
) as HTMLCanvasElement
if (!this.outputCanvas) {
this.outputCanvas = this._createCanvas(
FACE_LANDMARKS_CANVAS_ID,
PROCESSING_WIDTH,
PROCESSING_HEIGHT
)
}
this.outputCanvasCtx = this.outputCanvas.getContext('2d')!
}
_createCanvas(id: string, width: number, height: number) {
const element = document.createElement('canvas')
element.setAttribute('id', id)
element.setAttribute('width', '' + width)
element.setAttribute('height', '' + height)
return element
}
update(opts: BackgroundOptions): void {
this.options = opts
}
async restart(opts: ProcessorOptions<Track.Kind>) {
await this.destroy()
return this.init(opts)
}
async destroy() {
this.timerWorker?.postMessage({
id: CLEAR_TIMEOUT,
})
this.timerWorker?.terminate()
this.faceLandmarker?.close()
}
clone() {
return new FaceLandmarksProcessor(this.options)
}
serialize() {
return {
type: this.type,
options: this.options,
}
}
}
@@ -3,10 +3,13 @@ import { Track, TrackProcessor } from 'livekit-client'
import { BackgroundBlurTrackProcessorJsWrapper } from './BackgroundBlurTrackProcessorJsWrapper'
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
import { BackgroundVirtualTrackProcessorJsWrapper } from './BackgroundVirtualTrackProcessorJsWrapper'
import { FaceLandmarksProcessor } from './FaceLandmarksProcessor'
export type BackgroundOptions = {
blurRadius?: number
imagePath?: string
showGlasses?: boolean
showFrench?: boolean
}
export interface ProcessorSerialized {
@@ -25,11 +28,16 @@ export interface BackgroundProcessorInterface
export enum ProcessorType {
BLUR = 'blur',
VIRTUAL = 'virtual',
FACE_LANDMARKS = 'faceLandmarks',
}
export class BackgroundProcessorFactory {
static isSupported() {
return ProcessorWrapper.isSupported || BackgroundCustomProcessor.isSupported
return (
ProcessorWrapper.isSupported ||
BackgroundCustomProcessor.isSupported ||
FaceLandmarksProcessor.isSupported
)
}
static getProcessor(
@@ -50,6 +58,10 @@ export class BackgroundProcessorFactory {
if (BackgroundCustomProcessor.isSupported) {
return new BackgroundCustomProcessor(opts)
}
} else if (type === ProcessorType.FACE_LANDMARKS) {
if (FaceLandmarksProcessor.isSupported) {
return new FaceLandmarksProcessor(opts)
}
}
return undefined
}
@@ -4,6 +4,7 @@ import { FullScreenMenuItem } from './FullScreenMenuItem'
import { SettingsMenuItem } from './SettingsMenuItem'
import { FeedbackMenuItem } from './FeedbackMenuItem'
import { EffectsMenuItem } from './EffectsMenuItem'
import { SupportMenuItem } from './SupportMenuItem'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = () => {
@@ -20,6 +21,7 @@ export const OptionsMenuItems = () => {
</MenuSection>
<Separator />
<MenuSection>
<SupportMenuItem />
<FeedbackMenuItem />
<SettingsMenuItem />
</MenuSection>
@@ -0,0 +1,27 @@
import { RiQuestionLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { Crisp } from 'crisp-sdk-web'
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
export const SupportMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const isSupportEnabled = useIsSupportEnabled()
if (!isSupportEnabled || !Crisp) {
return
}
return (
<MenuItem
className={menuRecipe({ icon: true, variant: 'dark' }).item}
onAction={() => {
Crisp?.chat.open()
}}
>
<RiQuestionLine size={20} />
{t('support')}
</MenuItem>
)
}
@@ -1,60 +0,0 @@
import { ToggleButton } from '@/primitives'
import { RiQuestionLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Crisp } from 'crisp-sdk-web'
import { useEffect, useState } from 'react'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const [isOpened, setIsOpened] = useState(() => {
return window?.$crisp?.is?.('chat:opened') || false
})
useEffect(() => {
if (!Crisp) {
return
}
Crisp.chat.onChatOpened(() => {
setIsOpened(true)
})
Crisp.chat.onChatClosed(() => {
setIsOpened(false)
})
return () => {
Crisp.chat.offChatOpened()
Crisp.chat.offChatClosed()
}
}, [])
const isSupportEnabled = useIsSupportEnabled()
if (!isSupportEnabled) {
return
}
return (
<ToggleButton
square
variant="primaryTextDark"
tooltip={t('support')}
aria-label={t('support')}
isSelected={isOpened}
onPress={(e) => {
if (isOpened) {
Crisp.chat.close()
} else {
Crisp.chat.open()
}
onPress?.(e)
}}
data-attr="controls-support"
{...props}
>
<RiQuestionLine />
</ToggleButton>
)
}
@@ -13,6 +13,7 @@ export const Effects = () => {
<div
className={css({
padding: '0 1.5rem',
overflowY: 'scroll',
})}
>
<EffectsConfiguration
@@ -5,17 +5,22 @@ import {
BackgroundProcessorFactory,
BackgroundProcessorInterface,
ProcessorType,
BackgroundOptions,
} from '../blur'
import { css } from '@/styled-system/css'
import { Text, P, ToggleButton, H } from '@/primitives'
import { styled } from '@/styled-system/jsx'
import { BackgroundOptions } from '@livekit/track-processors'
import { BlurOn } from '@/components/icons/BlurOn'
import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import { RiProhibited2Line } from '@remixicon/react'
import {
RiProhibited2Line,
RiGlassesLine,
RiGoblet2Fill,
} from '@remixicon/react'
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
enum BlurRadius {
NONE = 0,
@@ -50,6 +55,7 @@ export const EffectsConfiguration = ({
const { toggle, enabled } = useTrackToggle({ source: Track.Source.Camera })
const [processorPending, setProcessorPending] = useState(false)
const processorPendingReveal = useSyncAfterDelay(processorPending)
const hasFaceLandmarksAccess = useHasFaceLandmarksAccess()
useEffect(() => {
const videoElement = videoRef.current
@@ -139,9 +145,42 @@ export const EffectsConfiguration = ({
}
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
if (type === ProcessorType.FACE_LANDMARKS) {
const effect = options.showGlasses ? 'glasses' : 'french'
return t(
`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`
)
}
return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
}
const getFaceLandmarksOptions = () => {
const processor = getProcessor()
if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) {
return processor.serialize().options as {
showGlasses?: boolean
showFrench?: boolean
}
}
return { showGlasses: false, showFrench: false }
}
const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'french') => {
const currentOptions = getFaceLandmarksOptions()
const newOptions = {
...currentOptions,
[effect === 'glasses' ? 'showGlasses' : 'showFrench']:
!currentOptions[effect === 'glasses' ? 'showGlasses' : 'showFrench'],
}
if (!newOptions.showGlasses && !newOptions.showFrench) {
// If both effects are off stop the processor
await clearEffect()
} else {
await toggleEffect(ProcessorType.FACE_LANDMARKS, newOptions)
}
}
return (
<div
className={css(
@@ -302,6 +341,68 @@ export const EffectsConfiguration = ({
</ToggleButton>
</div>
</div>
{hasFaceLandmarksAccess && (
<div
className={css({
marginTop: '1.5rem',
})}
>
<H
lvl={3}
style={{
marginBottom: '1rem',
}}
variant="bodyXsBold"
>
{t('faceLandmarks.title')}
</H>
<div
className={css({
display: 'flex',
gap: '1.25rem',
})}
>
<ToggleButton
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showFrench: false,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showFrench: false,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleFaceLandmarkEffect('glasses')
}
isSelected={getFaceLandmarksOptions().showGlasses}
data-attr="toggle-glasses"
>
<RiGlassesLine />
</ToggleButton>
<ToggleButton
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showFrench: true,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showFrench: true,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleFaceLandmarkEffect('french')
}
isSelected={getFaceLandmarksOptions().showFrench}
data-attr="toggle-french"
>
<RiGoblet2Fill />
</ToggleButton>
</div>
</div>
)}
<div
className={css({
marginTop: '1.5rem',
@@ -0,0 +1,9 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
export const useHasFaceLandmarksAccess = () => {
const featureEnabled = useFeatureFlagEnabled('face-landmarks')
const isAnalyticsEnabled = useIsAnalyticsEnabled()
return featureEnabled || !isAnalyticsEnabled
}
@@ -17,7 +17,6 @@ import {
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { SupportToggle } from '../../components/controls/SupportToggle'
import { useSidePanel } from '../../hooks/useSidePanel'
import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
@@ -138,10 +137,6 @@ export function MobileControlBar({
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<SupportToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<Button
onPress={() => {
toggleEffects()
@@ -1,7 +1,6 @@
import { css } from '@/styled-system/css'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { SupportToggle } from '../../components/controls/SupportToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { AdminToggle } from '../../components/AdminToggle'
import { useSize } from '../../hooks/useResizeObserver'
@@ -14,13 +13,15 @@ import { useTranslation } from 'react-i18next'
const CONTROL_BAR_BREAKPOINT = 1100
const NavigationControls = ({ onPress }: Partial<ToggleButtonProps>) => (
const NavigationControls = ({
onPress,
tooltipType = 'instant',
}: Partial<ToggleButtonProps>) => (
<>
<ChatToggle onPress={onPress} />
<ParticipantsToggle onPress={onPress} />
<TranscriptToggle onPress={onPress} />
<SupportToggle onPress={onPress} />
<AdminToggle onPress={onPress} />
<ChatToggle onPress={onPress} tooltipType={tooltipType} />
<ParticipantsToggle onPress={onPress} tooltipType={tooltipType} />
<TranscriptToggle onPress={onPress} tooltipType={tooltipType} />
<AdminToggle onPress={onPress} tooltipType={tooltipType} />
</>
)
@@ -55,7 +56,7 @@ export const LateralMenu = () => {
gap: '0.5rem',
})}
>
<NavigationControls onPress={handleClose} />
<NavigationControls onPress={handleClose} tooltipType="delayed" />
</Dialog>
</Popover>
</DialogTrigger>
@@ -7,7 +7,7 @@ import {
} from '@livekit/components-core'
import { RoomEvent, Track } from 'livekit-client'
import * as React from 'react'
import { useState } from 'react'
import {
CarouselLayout,
ConnectionStateToast,
@@ -29,6 +29,7 @@ import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '../components/RecordingStateToast'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
const LayoutWrapper = styled(
'div',
@@ -149,6 +150,8 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const { isSidePanelOpen } = useSidePanel()
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
return (
<div
className="lk-video-conference"
@@ -162,6 +165,10 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
value={layoutContext}
// onPinChange={handleFocusStateChange}
>
<ScreenShareErrorModal
isOpen={isShareErrorVisible}
onClose={() => setIsShareErrorVisible(false)}
/>
<div
// todo - extract these magic values into constant
style={{
@@ -207,7 +214,18 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
</LayoutWrapper>
<MainNotificationToast />
</div>
<ControlBar />
<ControlBar
onDeviceError={(e) => {
console.error(e)
if (
e.source == Track.Source.ScreenShare &&
e.error.toString() ==
'NotAllowedError: Permission denied by system'
) {
setIsShareErrorVisible(true)
}
}}
/>
<SidePanel />
</LayoutContextProvider>
)}
@@ -1,84 +0,0 @@
import { authUrl, useUser } from '../auth'
export enum ClientMessageType {
ROOM_CREATED = 'ROOM_CREATED',
}
export class SdkReverseClient {
/**
* IDEA: Use API Key. Must be based on some sort of credentials? No needs for now as there are no security
* plausible at the moment.
*/
static getAllowTargetOrigin() {
return '*'
}
static post(type: ClientMessageType, data: unknown = {}) {
window.parent.postMessage(
{
type,
data,
},
SdkReverseClient.getAllowTargetOrigin()
)
}
static broadcastAuthentication() {
const bc = new BroadcastChannel('APP_CHANNEL')
bc.postMessage({ type: 'AUTHENTICATED' })
/**
* This means the parent window has authenticated has successfully refetched user, then we can close the popup.
*/
bc.onmessage = (event) => {
if (event.data.type === 'AUTHENTICATED_ACK') {
window.close()
}
}
}
static waitForAuthenticationAck() {
return new Promise<void>((resolve) => {
const bc = new BroadcastChannel('APP_CHANNEL')
bc.onmessage = async (event) => {
if (event.data.type === 'AUTHENTICATED') {
resolve()
bc.postMessage({ type: 'AUTHENTICATED_ACK' })
}
}
})
}
}
/**
* Returns a function to be awaited in order to make sure the user is logged in.
* If not logged-in it opens a popup with the connection flow, the promise returned is resolved
* once logged-in.
*
* To be used in SDK scope.
*/
export function useEnsureAuth() {
const { isLoggedIn, ...other } = useUser({
fetchUserOptions: { attemptSilent: false },
})
const startSSO = () => {
return new Promise<void>((resolve) => {
SdkReverseClient.waitForAuthenticationAck().then(async () => {
await other.refetch()
resolve()
})
const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=400,height=900,left=100,top=100`
window.open(new URL('authenticate/', authUrl()).href, '', params)
})
}
const ensureAuth = async () => {
if (!isLoggedIn) {
await startSSO()
}
}
return { ensureAuth }
}
@@ -0,0 +1,35 @@
import { fetchApi } from '@/api/fetchApi'
import { useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import { CallbackCreationRoomData } from '../utils/types'
export type CallbackResponse = {
status: string
room: CallbackCreationRoomData
}
export const fetchRoomGenerationState = async ({
callbackId,
}: {
callbackId: string
}) => {
return fetchApi<CallbackResponse>(`/rooms/creation-callback/`, {
method: 'POST',
body: JSON.stringify({
callback_id: callbackId,
}),
})
}
export const useRoomCreationCallback = ({
callbackId = '',
}: {
callbackId?: string
}) => {
return useQuery({
queryKey: [keys.roomCreationCallback, callbackId],
queryFn: () => fetchRoomGenerationState({ callbackId }),
enabled: !!callbackId,
refetchInterval: 1000,
})
}
@@ -1,103 +0,0 @@
import { Button } from '@/primitives/Button'
import { useTranslation } from 'react-i18next'
import { usePersistentUserChoices } from '@livekit/components-react'
import { useState } from 'react'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { css } from '@/styled-system/css'
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
import { VisioIcon } from '@/assets/VisioIcon'
import { generateRoomId, useCreateRoom } from '../../rooms'
import {
ClientMessageType,
SdkReverseClient,
useEnsureAuth,
} from '../SdkReverseClient'
export const SdkCreateButton = () => {
const { t } = useTranslation('sdk', { keyPrefix: 'createButton' })
const [roomUrl, setRoomUrl] = useState<string>()
const [isLoading, setIsLoading] = useState(false)
const {
userChoices: { username },
} = usePersistentUserChoices()
const { mutateAsync: createRoom } = useCreateRoom()
const { ensureAuth } = useEnsureAuth()
const submitCreateRoom = async () => {
setIsLoading(true)
const slug = generateRoomId()
const data = await createRoom({ slug, username })
const roomUrlTmp = getRouteUrl('room', data.slug)
setRoomUrl(roomUrlTmp)
setIsLoading(false)
SdkReverseClient.post(ClientMessageType.ROOM_CREATED, {
url: roomUrlTmp,
})
}
const submit = async () => {
await ensureAuth()
submitCreateRoom()
}
return (
<div
className={css({
paddingTop: '3px',
paddingLeft: '3px',
})}
>
{roomUrl ? (
<RoomUrl roomUrl={roomUrl} />
) : (
<Button
variant="primaryDark"
aria-label={t('label')}
onPress={submit}
data-attr="sdk-create"
loading={isLoading}
icon={<VisioIcon />}
>
{t('label')}
</Button>
)}
</div>
)
}
const RoomUrl = ({ roomUrl }: { roomUrl: string }) => {
const [isCopied, setIsCopied] = useState(false)
const copy = () => {
navigator.clipboard.writeText(roomUrl!)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 1000)
}
return (
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
})}
>
<span
className={css({
color: 'greyscale.600',
})}
>
{roomUrl}
</span>
<Button
variant={isCopied ? 'success' : 'quaternaryText'}
data-attr="sdk-create-copy"
onPress={copy}
square
>
{isCopied ? <RiCheckLine /> : <RiFileCopyLine />}
</Button>
</div>
)
}
@@ -0,0 +1,171 @@
import { Button } from '@/primitives/Button'
import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { HStack, VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { RiCloseLine, RiFileCopyLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { Spinner } from '@/primitives/Spinner'
import { buttonRecipe } from '@/primitives/buttonRecipe'
import { VisioIcon } from '@/assets/VisioIcon'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { useRoomCreationCallback } from '../api/useRoomCreationCallback'
import { PopupManager } from '../utils/PopupManager'
import { CallbackCreationRoomData } from '../utils/types'
import { useSearchParams } from 'wouter'
const popupManager = new PopupManager()
export const CreateMeetingButton = () => {
const { t } = useTranslation('sdk', { keyPrefix: 'createMeeting' })
const [searchParams] = useSearchParams()
const [callbackId, setCallbackId] = useState<string | undefined>(undefined)
const [isPending, setIsPending] = useState(false)
const initialRoom = useMemo(() => {
const roomSlug = searchParams.get('slug')
if (!roomSlug) return undefined
return {
slug: roomSlug.trim(), // Trim whitespace for safety
}
}, [searchParams])
const [room, setRoom] = useState<CallbackCreationRoomData | undefined>(
initialRoom
)
const { data } = useRoomCreationCallback({ callbackId })
const roomUrl = useMemo(() => {
if (room?.slug) return getRouteUrl('room', room.slug)
}, [room])
useEffect(() => {
if (!data?.room?.slug) return
setRoom(data.room)
setCallbackId(undefined)
setIsPending(false)
popupManager.sendRoomData({
room: {
url: getRouteUrl('room', data.room.slug),
...data.room,
},
})
}, [data])
useEffect(() => {
popupManager.setupMessageListener(
(id) => setCallbackId(id),
(data) => {
setRoom(data)
setIsPending(false)
}
)
return () => popupManager.cleanup()
}, [])
const resetState = () => {
setRoom(undefined)
setCallbackId(undefined)
setIsPending(false)
popupManager.clearState()
}
if (isPending) {
return (
<div>
<Spinner />
</div>
)
}
return (
<div
className="p-6"
style={{
display: 'flex',
justifyContent: 'start',
alignItems: 'start',
border: 'none',
}}
>
{roomUrl && room?.slug ? (
<VStack justify={'start'} alignItems={'start'} gap={0.25}>
<HStack>
<Link
className={buttonRecipe({ size: 'sm' })}
href={roomUrl}
target="_blank"
style={{
textWrap: 'nowrap',
}}
>
<VisioIcon />
{t('joinButton')}
</Link>
<HStack gap={0}>
<Button
variant="quaternaryText"
square
icon={<RiFileCopyLine />}
tooltip={t('copyLinkTooltip')}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
}}
/>
{searchParams.get('readOnly') === 'false' && (
<Button
variant="quaternaryText"
square
icon={<RiCloseLine />}
onPress={resetState}
aria-label={t('resetLabel')}
/>
)}
</HStack>
</HStack>
<VStack justify={'start'} alignItems="start" gap={0.25}>
<Text variant={'smNote'} margin={false} centered={false}>
{roomUrl.replace('https://', '')}
</Text>
<Text variant={'smNote'} margin={false} centered={false}>
{t('participantLimit')}
</Text>
</VStack>
</VStack>
) : (
<div
className={css({
minHeight: '46px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
})}
>
{/*
* Using popup for Visio to access session cookies (blocked in iframes).
* If authenticated: Popup creates room and returns data directly.
* If not: Popup sends callbackId, redirects to login, then backend
* associates new room with callbackId after authentication.
*/}
<Button
onPress={() => {
setIsPending(true)
popupManager.createPopupWindow(() => {
setIsPending(false)
})
}}
size="sm"
>
<VisioIcon />
{t('createButton')}
</Button>
</div>
)}
</div>
)
}
@@ -0,0 +1,76 @@
import { useEffect, useMemo } from 'react'
import { css } from '@/styled-system/css'
import { generateRoomId, useCreateRoom } from '../../rooms'
import { useUser } from '@/features/auth'
import { Spinner } from '@/primitives/Spinner'
import { CallbackIdHandler } from '../utils/CallbackIdHandler'
import { PopupWindow } from '../utils/PopupWindow'
const callbackIdHandler = new CallbackIdHandler()
const popupWindow = new PopupWindow()
export const CreatePopup = () => {
const { isLoggedIn } = useUser({ fetchUserOptions: { attemptSilent: false } })
const { mutateAsync: createRoom } = useCreateRoom()
const callbackId = useMemo(() => callbackIdHandler.getOrCreate(), [])
/**
* Handle unauthenticated users by redirecting to login
*
* When redirecting to authentication, the window.location change breaks the connection
* between this popup and its parent window. We need to send the callbackId to the parent
* before redirecting so it can re-establish connection after authentication completes.
* This prevents the popup from becoming orphaned and ensures state consistency.
*/
useEffect(() => {
if (isLoggedIn === false) {
// redirection loses the connection to the manager
// prevent it passing an async callback id
popupWindow.sendCallbackId(callbackId, () => {
popupWindow.navigateToAuthentication()
})
}
}, [isLoggedIn, callbackId])
/**
* Automatically create meeting room once user is authenticated
* This effect will trigger either immediately if the user is already logged in,
* or after successful authentication and return to this popup
*/
useEffect(() => {
const createMeetingRoom = async () => {
try {
const slug = generateRoomId()
const roomData = await createRoom({
slug,
callbackId,
})
// Send room data back to parent window and clean up resources
popupWindow.sendRoomData(roomData, () => {
callbackIdHandler.clear()
popupWindow.close()
})
} catch (error) {
console.error('Failed to create meeting room:', error)
}
}
if (isLoggedIn && callbackId) {
createMeetingRoom()
}
}, [isLoggedIn, callbackId, createRoom])
return (
<div
className={css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
})}
>
<Spinner />
</div>
)
}
@@ -0,0 +1,45 @@
export class CallbackIdHandler {
private storageKey = 'popup_callback_id'
private generateId(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
)
}
/**
* Gets an existing callback ID or creates a new one
*/
public getOrCreate(): string {
const existingId = this.get()
if (existingId) {
return existingId
}
const newId = this.generateId()
this.set(newId)
return newId
}
/**
* Gets the current callback ID if one exists
*/
public get(): string | null {
return sessionStorage.getItem(this.storageKey)
}
/**
* Sets a callback ID
*/
private set(id: string): void {
sessionStorage.setItem(this.storageKey, id)
}
/**
* Removes the current callback ID
*/
public clear(): void {
sessionStorage.removeItem(this.storageKey)
}
}
@@ -0,0 +1,77 @@
import { getRouteUrl } from '@/navigation/getRouteUrl'
import {
CallbackCreationRoomData,
ClientMessageType,
PopupMessageData,
PopupMessageType,
} from './types'
export class PopupManager {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private messageHandler: (event: MessageEvent<any>) => void = () => {}
public createPopupWindow(onFailure: () => void) {
const popupWindow = window.open(
`${window.location.origin}/sdk/create-popup`,
'CreatePopupWindow',
`status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes`
)
if (popupWindow) {
popupWindow.focus()
} else {
onFailure()
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private messageParent(type: ClientMessageType, data: any) {
window?.parent.postMessage(
{
type: type,
data: data,
},
'*'
)
}
public clearState() {
this.messageParent(ClientMessageType.STATE_CLEAR, {})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public sendRoomData(data: { room: any }) {
this.messageParent(ClientMessageType.ROOM_CREATED, data)
}
public setupMessageListener(
onCallbackId: (id: string) => void,
onRoomData: (data: CallbackCreationRoomData) => void
) {
this.messageHandler = (event) => {
const data = event.data as PopupMessageData
// Skip messages from untrusted sources
if (data.source !== window.location.origin) return
switch (data.type) {
case PopupMessageType.CALLBACK_ID:
onCallbackId(data.callbackId as string)
return
case PopupMessageType.ROOM_DATA:
if (!data?.room) return
onRoomData(data.room)
this.sendRoomData({
room: {
url: getRouteUrl('room', data.room.slug),
...data.room,
},
})
return
}
}
window.addEventListener('message', this.messageHandler)
}
public cleanup() {
window.removeEventListener('message', this.messageHandler)
}
}
@@ -0,0 +1,50 @@
import { authUrl } from '@/features/auth'
import { PopupMessageType, CallbackCreationRoomData } from './types'
export class PopupWindow {
private sendMessageToManager(
type: PopupMessageType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any,
callback?: () => void
) {
if (!window.opener) {
console.error('No manager window found')
window.close()
return
}
window.opener.postMessage(
{
source: window.location.origin,
type,
...data,
},
window.location.origin
)
callback?.()
}
public sendRoomData(data: CallbackCreationRoomData, callback?: () => void) {
this.sendMessageToManager(
PopupMessageType.ROOM_DATA,
{ room: { slug: data.slug } },
callback
)
}
public sendCallbackId(callbackId: string, callback?: () => void) {
this.sendMessageToManager(
PopupMessageType.CALLBACK_ID,
{ callbackId },
callback
)
}
public close() {
window.close()
}
public navigateToAuthentication() {
window.location.href = authUrl({})
}
}
@@ -0,0 +1,20 @@
export type CallbackCreationRoomData = {
slug: string
}
export enum ClientMessageType {
ROOM_CREATED = 'ROOM_CREATED',
STATE_CLEAR = 'STATE_CLEAR',
}
export interface PopupMessageData {
type: PopupMessageType
source: string
callbackId?: string
room?: CallbackCreationRoomData
}
export enum PopupMessageType {
CALLBACK_ID,
ROOM_DATA,
}
@@ -18,15 +18,16 @@ export const terminateSupportSession = () => {
export type useSupportProps = {
id?: string
isDisabled?: boolean
}
// Configure Crisp chat for real-time support across all pages.
export const useSupport = ({ id }: useSupportProps) => {
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
useEffect(() => {
if (!id || Crisp.isCrispInjected()) return
if (!id || Crisp.isCrispInjected() || isDisabled) return
Crisp.configure(id)
Crisp.setHideOnMobile(true)
}, [id])
}, [id, isDisabled])
return null
}
+1 -1
View File
@@ -106,7 +106,7 @@ const Marianne = () => {
className={css({
letterSpacing: '-.01em',
textTransform: 'uppercase',
fontWeight: '600',
fontWeight: '700',
fontFamily: 'Marianne',
fontSize: '1.25rem',
lineHeight: '1.75rem',
+1 -1
View File
@@ -44,7 +44,7 @@ const Marianne = () => {
className={css({
letterSpacing: '-.01em',
textTransform: 'uppercase',
fontWeight: '600',
fontWeight: '700',
fontFamily: 'Marianne',
fontSize: '0.7875rem !important',
})}
@@ -29,12 +29,6 @@
"option2": "",
"option3": ""
}
},
"dataProtection": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
}
}
+7 -39
View File
@@ -6,6 +6,7 @@
"contact": {
"title": "",
"address": "",
"city": "",
"phone": "",
"siret": "",
"siren": ""
@@ -17,53 +18,20 @@
},
"hosting": {
"title": "",
"body": "",
"address": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
"body": ""
},
"accessibility": {
"title": "",
"body": "",
"status": "",
"reporting": {
"title": "",
"body1": "",
"body2": "",
"address": {
"line1": "",
"line2": "",
"line3": ""
}
}
"more": "",
"link": "",
"status": ""
},
"reuse": {
"title": "",
"body1": "",
"license": "",
"body2": "",
"media": {
"title": "",
"body": ""
}
},
"liability": {
"title": "",
"body1": "",
"body2": "",
"body3": "",
"body4": ""
},
"dataProtection": {
"title": "",
"body": "",
"address": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
"body3": ""
}
}
+37 -14
View File
@@ -60,6 +60,17 @@
"createRoom": {
"heading": "",
"body": ""
},
"screenShare": {
"title": "",
"ariaLabel": "",
"message": "",
"macInstructions": "",
"macSystemPreferences": "",
"helpLinkText": "",
"helpLinkLabel": "",
"closeButton": "",
"newTab": ""
}
},
"controls": {
@@ -101,7 +112,6 @@
"open": "",
"closed": ""
},
"support": "",
"moreOptions": "",
"reactions": {
"button": "",
@@ -120,26 +130,38 @@
"fullscreen": {
"enter": "",
"exit": ""
}
},
"support": ""
}
},
"effects": {
"activateCamera": "",
"notAvailable": "",
"heading": "",
"activateCamera": "Ihre Kamera ist deaktiviert. Wählen Sie eine Option, um sie zu aktivieren.",
"notAvailable": "Videoeffekte werden in Kürze in Ihrem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit können Sie Google Chrome für beste Leistung oder Firefox verwenden :(",
"heading": "Unschärfe",
"blur": {
"title": "",
"light": "",
"normal": "",
"apply": "",
"clear": ""
"title": "Hintergrundunschärfe",
"light": "Leichte Unschärfe",
"normal": "Unschärfe",
"apply": "Unschärfe aktivieren",
"clear": "Unschärfe deaktivieren"
},
"virtual": {
"title": "",
"apply": "",
"clear": ""
"title": "Virtueller Hintergrund",
"apply": "Virtuellen Hintergrund aktivieren",
"clear": "Virtuellen Hintergrund deaktivieren"
},
"experimental": ""
"faceLandmarks": {
"title": "Visuelle Effekte",
"glasses": {
"apply": "Brille hinzufügen",
"clear": "Brille entfernen"
},
"french": {
"apply": "Französische Touch hinzufügen",
"clear": "Französische Touch entfernen"
}
},
"experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität."
},
"sidePanel": {
"heading": {
@@ -178,6 +200,7 @@
"access": {
"title": "",
"description": "",
"type": "",
"levels": {
"public": {
"label": "",
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": ""
"createMeeting": {
"createButton": "",
"joinButton": "",
"copyLinkTooltip": "",
"resetLabel": "",
"participantLimit": "",
"popupBlocked": ""
}
}
+66 -102
View File
@@ -1,108 +1,72 @@
{
"terms": {
"title": "",
"introduction": "",
"articles": {
"article1": {
"title": "",
"content": ""
},
"article2": {
"title": "",
"content": "",
"purposes": []
},
"article3": {
"title": "",
"definitions": []
},
"article4": {
"title": "",
"content": ""
},
"article5": {
"title": "",
"sections": {
"section1": {
"title": "",
"content": "",
"paragraph1": "",
"paragraph2": "",
"capabilities": [],
"paragraph3": ""
},
"section2": {
"title": "",
"content": "",
"paragraph": "",
"capabilities": []
}
}
},
"article6": {
"title": "",
"sections": {
"section1": {
"title": "",
"paragraphs": []
},
"section2": {
"title": "",
"paragraphs": []
}
}
},
"article7": {
"title": "",
"sections": {
"section1": {
"title": "",
"content": ""
},
"section2": {
"title": "",
"content": "",
"dataCategories": []
},
"section3": {
"title": "",
"content": "",
"purposes": []
},
"section4": {
"title": "",
"content": "",
"bases": [],
"details": []
},
"section5": {
"title": "",
"content": "",
"dataType": "",
"retentionPeriod": "",
"retentionTable": [],
"additionalInfo": ""
},
"section6": {
"title": "",
"content": ""
},
"section7": {
"title": "",
"content": "",
"rights": [],
"exerciseRights": {
"content": "",
"methods": []
},
"additionalInfo": []
},
"section8": {
"title": "",
"paragraphs": []
}
"title": "",
"articles": {
"article1": {
"title": "",
"content": ""
},
"article2": {
"title": "",
"content": "",
"purposes": ""
},
"article3": {
"title": "",
"definition": ""
},
"article4": {
"title": "",
"content": ""
},
"article5": {
"title": "",
"sections": {
"section1": {
"title": "",
"content": "",
"paragraph1": "",
"paragraph2": "",
"capabilities": ["", "", "", ""],
"paragraph3": ""
},
"section2": {
"title": "",
"content": "",
"paragraph": "",
"capabilities": ["", "", "", "", "", "", ""]
}
}
},
"article6": {
"title": "",
"sections": {
"section1": {
"title": "",
"paragraphs": ["", ""]
},
"section2": {
"title": "",
"paragraphs": [""]
}
}
},
"article7": {
"title": "",
"sections": {
"section1": {
"title": "",
"content": "",
"paragraphs": ["", "", ""]
},
"section2": {
"title": "",
"content": ""
}
}
},
"article8": {
"title": "",
"content": ""
}
}
}
+12 -18
View File
@@ -1,40 +1,34 @@
{
"accessibility": {
"title": "Accessibility",
"introduction": "Visio is committed to making its digital services accessible, in accordance with Article 47 of Law No. 2005-102 of February 11, 2005.",
"introduction": "<i>Visio</i> is committed to making its digital services accessible, in accordance with Article 47 of Law No. 2005-102 of February 11, 2005.",
"declaration": {
"title": "Accessibility Declaration",
"date": "Established on March 5, 2025."
"title": "Accessibility Statement",
"date": "Established on December 4, 2024."
},
"scope": "This accessibility declaration applies to the site visio.numerique.gouv.fr",
"scope": "This accessibility statement applies to the website visio.numerique.gouv.fr",
"complianceStatus": {
"title": "Compliance Status",
"body": "visio.numerique.gouv.fr is not compliant with RGAA 4.1. The site has not yet been audited. However, the team is striving to create a site accessible to all by following RGAA recommendations."
"body": "visio.numerique.gouv.fr is not compliant with RGAA 4.1. The site has not yet been audited. However, the team strives to create a site that is accessible to everyone by following RGAA recommendations."
},
"improvement": {
"title": "Improvement and Contact",
"body": "If you are unable to access content or a service, you can contact the administrator of lasuite.numerique.gouv.fr to be directed to an accessible alternative or obtain the content in another format.",
"body": "If you cannot access content or a service, you can contact the person responsible for lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.",
"contact": {
"email": "Email: visio@numerique.gouv.fr",
"address": "Address: DINUM, 20 avenue de Ségur 75007 Paris"
},
"response": "We try to respond within 2 business days."
"response": "We try to respond within 2 working days."
},
"recourse": {
"title": "Recourse",
"introduction": "This procedure should be used in the following case: you have reported an accessibility issue to the website administrator that prevents you from accessing content or services on the portal, and you have not received a satisfactory response.",
"title": "Means of Recourse",
"introduction": "This procedure is to be used in the following case: you have reported to the website manager an accessibility issue that prevents you from accessing content or one of the portal's services, and you have not received a satisfactory response.",
"options": {
"intro": "You can:",
"option1": "Send a message to the Defender of Rights",
"option2": "Contact the delegate of the Defender of Rights in your region",
"option3": "Send a letter by mail (free, do not put a stamp):"
"option1": "Write a message to the Rights Defender",
"option2": "Contact the Rights Defender's representative in your region",
"option3": "Send a letter by mail (free, no stamp needed): </br> Rights Defender Free response 71120 75342 Paris CEDEX 07"
}
},
"dataProtection": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75007 Paris"
}
}
}
+17 -49
View File
@@ -1,69 +1,37 @@
{
"title": "Legal Notices",
"title": "Legal Notice",
"creator": {
"title": "Publisher",
"body": "The Visio site is published by the interministerial digital directorate (DINUM), hereinafter referred to as \"the publisher\", belonging to the Prime Minister's services.",
"body": "The Visio service is published by the Interministerial Digital Division of the State (DINUM) located at:",
"contact": {
"title": "Contact Information",
"address": "Address: {{address}}",
"phone": "Reception Tel.: {{phone}}",
"siret": "SIRET: {{siret}}",
"siren": "SIREN: {{siren}}"
"address": "20 avenue de Ségur",
"city": "75007 Paris",
"phone": "Reception Tel.: 01.71.21.01.70",
"siret": "SIRET: 12000101100010 (government general secretariat)",
"siren": "SIREN: 120 001 011"
}
},
"director": {
"title": "Publication Director",
"body": "Interministerial Digital Director."
"body": "The publication director is Madame Stéphanie Schaer, Interministerial Digital Director."
},
"hosting": {
"title": "Hosting",
"body": "This website is hosted by Outscale with SecNumCloud certification:",
"address": {
"line1": "Outscale",
"line2": "1, rue Royale",
"line3": "319 Bureaux de la Colline",
"line4": "92210 Saint-Cloud"
}
"body": "The service is hosted by Outscale West-1 CloudGouv SecNumCloud located in France."
},
"accessibility": {
"title": "Accessibility",
"body": "Compliance with digital accessibility standards is a future objective. The site has not yet been audited. However, the team strives to create a site accessible to all by following the RGAA recommendations.",
"status": "Accessibility status: non-compliant",
"reporting": {
"title": "Report a malfunction",
"body1": "If you encounter an accessibility issue preventing you from accessing content or a site feature, please let us know by contacting visio@numerique.gouv.fr.",
"body2": "If you do not receive a prompt response from us, you have the right to send your grievances or a request for referral to the Rights Defender. You have the right to send your grievances or request for referral to the \"Rights Defender\" by phone at 09 69 39 00 00, via the online form https://formulaire.defenseurdesdroits.fr or by free mail (no postage required):",
"address": {
"line1": "Défenseur des droits",
"line2": "Libre réponse 71120",
"line3": "75342 Paris CEDEX 07"
}
}
"body": "Compliance with digital accessibility standards is a future objective. The site has not yet been audited. However, the team strives to create a site accessible to all by following RGAA recommendations.",
"more": "For more information: ",
"link": "link to accessibility page",
"status": "Accessibility status: non-compliant"
},
"reuse": {
"title": "Content Reuse and Links",
"body1": "Unless explicitly mentioned as intellectual property held by third parties, the contents of this site are offered under an open license.",
"body2": "Any public or private site is authorized to establish, without prior authorization, a link (including deep linking) to the information disseminated on this site.",
"media": {
"title": "Graphics, Photos and Videos",
"body": "Graphics, photographs, and multimedia resources are most often covered by intellectual property rights held by third parties and are not, as such, reusable without their consent. Therefore, we invite you to contact us if you wish to reuse such content so that we can direct you to the holder(s) of these rights: visio@numerique.gouv.fr."
}
},
"liability": {
"title": "Liability Clause",
"body1": "The information offered on this site is provided as a service to public service agents, particularly to agents of State services.",
"body2": "The information and/or documents available on this site are subject to change at any time and may be updated to facilitate access to digital resources.",
"body3": "The information and/or documents accessed from this site are the responsibility of the owners of each interministerial service thus made available.",
"body4": "The publisher cannot under any circumstances be held responsible for any damage of any nature whatsoever resulting from the misuse of resources accessible from this site."
},
"dataProtection": {
"title": "Data Protection Officer",
"body": "If you wish to obtain information about personal data processing managed by a Prime Minister's Service, you can contact the data protection officer (DPO) by email at dpd@pm.gouv.fr or by mail at the following address:",
"address": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75700 Paris"
}
"body1": "Unless explicitly mentioned of intellectual property held by third parties, the contents of this site are offered under ",
"license": "Etalab 2.0 open license",
"body2": "You are particularly free to reproduce, copy, modify, extract, transform, communicate, distribute, redistribute, publish, transmit and exploit them provided you mention their source, their date of last update and do not mislead third parties regarding the information contained therein.",
"body3": "Any public or private site is authorized to establish, without prior authorization, a link (including deep link) to the information disseminated on this site."
}
}
+25 -2
View File
@@ -61,6 +61,17 @@
"createRoom": {
"heading": "Authentication Required",
"body": "This room has not been created yet. Please authenticate to create it or wait for an authenticated user to do so."
},
"screenShare": {
"title": "Unable to share your screen",
"ariaLabel": "Unable to share your screen",
"message": "Your browser may not be allowed to record the screen on your computer.",
"macInstructions": "Go to your",
"macSystemPreferences": "System Preferences",
"helpLinkText": "To learn more, see",
"helpLinkLabel": "Presentation issue",
"closeButton": "Dismiss",
"newTab": "New window"
}
},
"controls": {
@@ -100,7 +111,6 @@
"open": "Hide admin",
"closed": "Open admin"
},
"support": "Support",
"moreOptions": "More options",
"reactions": {
"button": "Send reaction",
@@ -119,7 +129,8 @@
"fullscreen": {
"enter": "Fullscreen",
"exit": "Exit fullscreen mode"
}
},
"support": "Contact support"
}
},
"effects": {
@@ -138,6 +149,17 @@
"apply": "Enable virtual background",
"clear": "Disable virtual background"
},
"faceLandmarks": {
"title": "Visual Effects",
"glasses": {
"apply": "Add Glasses",
"clear": "Remove Glasses"
},
"french": {
"apply": "Add French touch",
"clear": "Remove French touch"
}
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
},
"sidePanel": {
@@ -177,6 +199,7 @@
"access": {
"title": "Room access",
"description": "These settings will also apply to future occurrences of this meeting.",
"type": "Meeting access types",
"levels": {
"public": {
"label": "Open",
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Create a Visio link"
"createMeeting": {
"createButton": "Create",
"joinButton": "Join with Visio",
"copyLinkTooltip": "Copy link",
"resetLabel": "Reset",
"participantLimit": "Up to 150 participants.",
"popupBlocked": "Popup was blocked. Please allow popups for this site."
}
}
+88 -215
View File
@@ -1,221 +1,94 @@
{
"terms": {
"title": "Terms of Service",
"introduction": "These terms of service (referred to as \"ToS\") establish the legal framework of the \"Visio\" Platform and define the conditions of access and use of services by the user.",
"articles": {
"article1": {
"title": "Article 1 - Scope",
"content": "The platform \"visio.numerique.gouv.fr\" is an initiative of the interministerial operator of the Interministerial Digital Direction, which is responsible for processing and the operational carrier of the solution."
},
"article2": {
"title": "Article 2 - Purpose",
"content": "The platform may collect personal data for the following purposes:",
"purposes": [
"Promote team cooperation and remote work.",
"Facilitate the organization of meetings, conferences or training sessions.",
"Improve the Visio platform."
]
},
"article3": {
"title": "Article 3 - Definitions",
"definitions": [
{
"term": "User",
"definition": "Any public agent or interlocutor of a public agent, using the platform."
},
{
"term": "Services",
"definition": "The functionalities offered by the platform to meet its purposes."
},
{
"term": "Data controller",
"definition": "The person who, within the meaning of Article 4 of Regulation (EU) No. 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, determines the purposes and means of processing personal data."
}
]
},
"article4": {
"title": "Article 4 - Framework and usage restrictions",
"content": "Visio is in the process of certification for a non-sensitive data usage framework, which excludes the processing of so-called \"sensitive\" personal data under Articles 9 and 10 of the GDPR and information bearing the mention \"Restricted Distribution\" within the meaning of interministerial instruction 901/SGDSN/ANSSI."
},
"article5": {
"title": "Article 5 - Features",
"sections": {
"section1": {
"title": "5.1 - Features open to public agents",
"content": "Visio offers any public agent the possibility to create a videoconference room from a standard workstation, linked to their ProConnect account.",
"paragraph1": "Any State agent, whose activities are compatible with the use of Visio or who is not subject to particular confidentiality obligations or professional secrecy, can create a videoconference freely. They can then share the meeting link with other participants, whether they are part of the administration or not.",
"paragraph2": "For this purpose, each room organizer can:",
"capabilities": [
"Protect access to the room with a waiting room;",
"Authorize the entry of each participant in the room;",
"Mute participants' microphones and cameras;",
"Exclude a participant from the room."
],
"paragraph3": "Only agents whose field of activity is authorized to use Visio or who are not subject to a particular confidentiality obligation or professional secrecy can use or control this service and its functionalities."
},
"section2": {
"title": "5.2 - Features open to all Users",
"content": "Visio allows any user, public agent or not, to participate in any Visio room to which they are invited by a State agent and to benefit from all its features.",
"paragraph": "For this purpose, each participant can:",
"capabilities": [
"Decline their identity or indicate it when entering the room;",
"Exchange remotely in audio and/or video;",
"Share their screen;",
"Exchange messages during the room publicly with everyone;",
"See information about the conference;",
"Exercise their right to their image by choosing never to activate their camera and/or microphone or to deactivate one, the other or both at any time;",
"Protect the privacy of the place from which they access the room by selecting and then activating a screen background before entering the room or even during the videoconference."
]
}
}
},
"article6": {
"title": "Article 6 - Responsibility",
"sections": {
"section1": {
"title": "6.1 - The Platform Editor",
"paragraphs": [
"The editor reserves the right to suspend or revoke access to the service if it considers that the use of the service is detrimental to its image or does not correspond to security requirements.",
"The editor is committed to securing the Platform, particularly by taking all necessary measures to guarantee the security and confidentiality of the information provided.",
"The editor provides the necessary and reasonable means to ensure continuous access, without financial counterpart for users, to the Platform. It reserves the freedom to evolve, modify or suspend, without notice, the platform for maintenance reasons or for any other reason deemed necessary."
]
},
"section2": {
"title": "6.2 - The Platform User",
"paragraphs": [
"Any information transmitted by the User is their sole responsibility. It is reminded that any person making a false declaration for themselves or for others is exposed, in particular, to the sanctions provided for in Article 441-1 of the Penal Code, providing for penalties of up to three years' imprisonment and a fine of 45,000 euros.",
"It is up to the User to ensure beforehand that they are authorized to use Visio or not to be subject to a particular confidentiality obligation or professional secrecy."
]
}
}
},
"article7": {
"title": "Article 7 - Protection of personal data",
"sections": {
"section1": {
"title": "7.1 - Data controller",
"content": "The data controller is the Interministerial Digital Direction, whose director is Ms. Stéphanie Schaer."
},
"section2": {
"title": "7.2 - Personal data processed",
"content": "The platform may process the following personal data:",
"dataCategories": [
{
"category": "Data relating to the organizing public agent:",
"items": [
"Email address of the organizing public agent and participants;",
"Organization;",
"First and last name;",
"Public IP address;"
]
},
{
"category": "Data relating to videoconferences;"
},
{
"category": "Data collected by the host:",
"items": [
"The public IP address of the client (terminal associated with the browser used)",
"The IP address of the server",
"Technical connection data relating to",
"the Browser (type, version no.)",
"the Client workstation operating system",
"the flows (bandwidth, throughput, packet loss, latency)",
"the image quality (resolution, number of frames/s) in sending and receiving."
]
}
]
},
"section3": {
"title": "7.3 - Purposes of processing",
"content": "The platform may process personal data for the following purposes:",
"purposes": [
"Promote team cooperation and remote work;",
"Facilitate the organization of meetings, conferences or training sessions;",
"Facilitate assistance to each user of the service.",
"Improve the Visio platform;"
]
},
"section4": {
"title": "7.4 - Legal basis for data processing",
"content": "The data processed on the occasion of this processing have several legal bases:",
"bases": [
"the legal obligation to which the data controller is subject within the meaning of Article 6-c of the GDPR;",
"the public interest mission to which the data controller is subject within the meaning of Article 6-e of the GDPR."
],
"details": [
{
"title": "a) Data relating to the organizing public agent",
"content": "This data processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller within the meaning of Article 6-e of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReference": "The public interest mission is notably established by: Article 6 of Decree No. 2019-1088 of October 25, 2019 relating to the State's information and communication system and to the interministerial digital direction"
},
{
"title": "b) Data relating to the web conference",
"content": "This data processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller within the meaning of Article 6-e of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReference": "The public interest mission is notably established by: Article 6 of Decree No. 2019-1088 of October 25, 2019 relating to the State's information and communication system and to the interministerial digital direction"
},
{
"title": "c) Connection data",
"content": "This data processing is necessary for compliance with a legal obligation to which the controller is subject within the meaning of Article 6-c of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReferences": [
"The LCEN Law No. 2004-575 of June 21, 2004 for confidence in the digital economy",
"Articles 1 and 3 of Decree No. 2011-219 of February 25, 2011"
]
}
]
},
"section5": {
"title": "7.5 - Retention period of processed data",
"content": "Personal data are kept for the purposes of possible requests for assistance or traceability for a conference, for the development of statistics:",
"dataType": "Data Type",
"retentionPeriod": "Retention Period",
"retentionTable": [
{
"dataType": "Data relating to the organizing agent",
"retention": "The data kept are anonymized and are deleted at the latest 12 months after the expiry date of the videoconference"
},
{
"dataType": "Data relating to the videoconference",
"retention": "The data are deleted at the latest 12 months after the expiry date of the videoconference"
}
],
"additionalInfo": "At the end of the retention period, the data relating to the organizing agent and the data relating to the videoconference can no longer be used by the data controller within the framework of the platform. They are destroyed at the latest 12 months after the expiry date of the videoconference."
},
"section6": {
"title": "7.6 - Security and confidentiality",
"content": "The data controller undertakes to take the necessary technical and organizational security measures to ensure confidentiality, integrity and protect data access."
},
"section7": {
"title": "7.7 - Rights of data subjects",
"content": "Pursuant to Article 13 of Regulation (EU) No. 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, each user whose data is collected in the context of the use or connection to the Visio platform is reminded that they have the following rights regarding their personal data:",
"rights": [
"Right to information;",
"Right of access to data;",
"Right of rectification and right of erasure of data;",
"Right to restriction of data"
],
"exerciseRights": {
"content": "You can exercise these rights by contacting by:",
"methods": [
"Electronic means: visio@numerique.gouv.fr",
"Postal means: Direction interministérielle du Numérique, 20 avenue de Ségur, 75007 Paris."
]
},
"additionalInfo": [
"Due to the obligation of security and confidentiality in the processing of personal data incumbent on the data controller, your request will only be processed if you provide proof of your identity. To help you in your approach, you will find here a model letter developed by the Cnil.",
"You have the possibility to object to the processing of your personal data. To help you in your approach, you will find here a model letter developed by the Cnil.",
"Response times: The data controller undertakes to respond to your request for access, rectification or objection or any other request for additional information within a reasonable time that should not exceed 1 month from the receipt of your request."
]
},
"section8": {
"title": "7.8 - Recipients",
"paragraphs": [
"The collected data are processed only by persons legally authorized to know the processed information.",
"These are agents, employees or other persons who may represent the legal entity holding a public service mission that uses the platform service."
]
}
"title": "Terms of Use for Visio",
"articles": {
"article1": {
"title": "1. Scope",
"content": "This document defines the terms of use for Visio, the videoconferencing service of the administration."
},
"article2": {
"title": "2. Purpose of the Platform",
"content": "The service promotes team cooperation and remote work, and facilitates the organization of meetings, conferences, or training sessions.",
"purposes": "Visio is developed and operated by DINUM. Any use of the service must comply with these terms of use."
},
"article3": {
"title": "3. Access to the Service",
"definition": "Visio is made available to public agents connected through ProConnect for the purpose of administering a videoconference room. The connected public agent can then transmit a link allowing access to the room to any person, under their responsibility."
},
"article4": {
"title": "4. Usage",
"content": "The use of the service is free of charge. To administer a videoconference room, it is necessary to connect through ProConnect."
},
"article5": {
"title": "5. Features",
"sections": {
"section1": {
"title": "5.1. Features Available to Public Agents",
"content": "Visio offers public agents the ability to create a videoconference room from a standard workstation, linked to their ProConnect account.",
"paragraph1": "Any State agent whose activities are compatible with the use of Visio or who is not subject to specific confidentiality obligations or professional secrecy, can create a videoconference. They can then share the meeting link with other participants, whether they are part of the administration or not.",
"paragraph2": "To this end, each room organizer can:",
"capabilities": [
"Protect access to the room with a waiting room;",
"Authorize the entry of each participant into the room;",
"Mute the microphones and cameras of participants;",
"Remove a participant from the room."
],
"paragraph3": "Only agents whose field of activity is authorized to use Visio or who <u>are not subject to specific confidentiality obligations or professional secrecy</u>, may use or have control of this service and its features."
},
"section2": {
"title": "5.2. Features Available to Users",
"content": "Visio allows any user, whether a public agent or not, to participate in any Visio room to which they are invited by a State agent and to benefit from all its features.",
"paragraph": "To this end, each participant can:",
"capabilities": [
"Set their displayed identity when entering the room;",
"Communicate remotely via audio and/or video;",
"Share their screen;",
"Exchange messages during the session publicly with everyone;",
"View information about the conference;",
"Enable or disable their camera and/or microphone at any time;",
"Protect the privacy of their workstation by selecting and activating a background before entering the room or even during the videoconference."
]
}
}
},
"article6": {
"title": "6. User Commitments and Responsibilities",
"sections": {
"section1": {
"title": "6.1 Compliant Uses",
"paragraphs": [
"The service is made available to public agents through ProConnect.",
"The user is responsible for the data or content they enter in the videoconference room's messaging service. They must ensure they only enter appropriate messages."
]
},
"section2": {
"title": "6.2 Prohibited Uses",
"paragraphs": [
"The user agrees not to enter content or information in the Visio room's messaging service that is contrary to current legal and regulatory provisions."
]
}
}
},
"article7": {
"title": "7. DINUM Commitments and Responsibilities",
"sections": {
"section1": {
"title": "7.1. Security and Platform Access",
"content": "DINUM is committed to securing Visio, notably by taking all necessary measures to ensure the security and confidentiality of the information provided.",
"paragraphs": [
"DINUM commits to providing the necessary and reasonable means to ensure continuous access to Visio.",
"DINUM reserves the right to evolve, modify, or suspend the service without notice for maintenance reasons or for any other reason deemed necessary.",
"DINUM reserves the right to suspend or delete a user account of the service that has violated these terms of use, without prejudice to any potential criminal and civil liability actions that could be taken against the user."
]
},
"section2": {
"title": "7.2 Open Source and Licenses",
"content": "The source code of Visio is free and available here: https://github.com/suitenumerique/meet\nThe content offered by DINUM is under an Open License, with the exception of logos and iconographic and photographic representations that may be governed by their own licenses."
}
}
},
"article8": {
"title": "8. Evolution of Terms of Use",
"content": "The terms of these terms of use may be modified or supplemented at any time, without notice, depending on changes made to the service, changes in legislation, or for any other reason deemed necessary.\nThese modifications and updates are binding on the user who must, consequently, regularly refer to this section to verify the terms in force."
}
}
}
@@ -1,10 +1,10 @@
{
"accessibility": {
"title": "Accessibilité",
"introduction": "Visio s'engage à rendre ses services numériques accessibles, conformément à l'article 47 de la loi n°2005-102 du 11 février 2005.",
"introduction": "<i>Visio</i> s'engage à rendre ses services numériques accessibles, conformément à l'article 47 de la loi n°2005-102 du 11 février 2005.",
"declaration": {
"title": "Déclaration d'accessibilité",
"date": "Établie le 05 mars 2025."
"date": "Établie le 04 décembre 2024."
},
"scope": "Cette déclaration d'accessibilité s'applique au site visio.numerique.gouv.fr",
"complianceStatus": {
@@ -27,14 +27,8 @@
"intro": "Vous pouvez :",
"option1": "Écrire un message au Défenseur des droits",
"option2": "Contacter le délégué du Défenseur des droits dans votre région",
"option3": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre) :"
"option3": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre) : </br> Défenseur des droits Libre réponse 71120 75342 Paris CEDEX 07"
}
},
"dataProtection": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75007 Paris"
}
}
}
+15 -47
View File
@@ -2,68 +2,36 @@
"title": "Mentions Légales",
"creator": {
"title": "Éditeur",
"body": "Le site de Visio est édité par la direction interministérielle du numérique (DINUM), ci-après nommé « l'éditeur », appartenant aux services du Premier ministre.",
"body": "Le service Visio est édité par la Direction interministérielle du numérique de l'Etat (DINUM) située :",
"contact": {
"title": "Coordonnées",
"address": "Adresse : {{address}}",
"phone": "Tel. accueil : {{phone}}",
"siret": "SIRET : {{siret}}",
"siren": "SIREN : {{siren}}"
"address": "20 avenue de Ségur",
"city": "75007 Paris",
"phone": "Tel. Accueil : 01.71.21.01.70",
"siret": "SIRET : 12000101100010 (secrétariat général du gouvernement)",
"siren": "SIREN : 120 001 011"
}
},
"director": {
"title": "Directeur de la publication",
"body": "Directeur interministériel du numérique."
"body": "La directrice de la publication est Madame Stéphanie Schaer, Directrice interministérielle du numérique."
},
"hosting": {
"title": "Hébergement",
"body": "Ce site internet est hébergé par chez l'hébergeur Outscale en SecNumCloud :",
"address": {
"line1": "Outscale",
"line2": "1, rue Royale",
"line3": "319 Bureaux de la Colline",
"line4": "92210 Saint-Cloud"
}
"body": "Le service est hébergé par Outscale West-1 CloudGouv SecNumCloud situé en France."
},
"accessibility": {
"title": "Accessibilité",
"body": "La conformité aux normes d'accessibilité numérique est un objectif ultérieur. Le site n'a pas encore été audité. Cependant, l'équipe s'efforce de créer un site accessible à tous en suivant les recommandations du RGAA.",
"status": "Etat d'accessibilité : non conforme",
"reporting": {
"title": "Signaler un dysfonctionnement",
"body1": "Si vous rencontrez un défaut d'accessibilité vous empêchant d'accéder à un contenu ou une fonctionnalité du site, merci de nous en faire part en nous contactant visio@numerique.gouv.fr.",
"body2": "Si vous n'obtenez pas de réponse rapide de notre part, vous êtes en droit de faire parvenir vos doléances ou une demande de saisine au Défenseur des droits. Vous êtes en droit de faire parvenir vos doléances ou demande de saisine au « défenseur des droits » par téléphone au 09 69 39 00 00, via le formulaire en ligne https://formulaire.defenseurdesdroits.fr ou encore par courrier gratuit (sans affranchissement) :",
"address": {
"line1": "Défenseur des droits",
"line2": "Libre réponse 71120",
"line3": "75342 Paris CEDEX 07"
}
}
"more": "Pour en savoir plus : ",
"link": "lien vers la page d'accessibilité",
"status": "Etat d'accessibilité : non conforme"
},
"reuse": {
"title": "Réutilisation des contenus et liens",
"body1": "Sauf mention explicite de propriété intellectuelle détenue par des tiers, les contenus de ce site sont proposés sous licence ouverte.",
"body2": "Tout site public ou privé est autorisé à établir, sans autorisation préalable, un lien (y compris profond) vers les informations diffusées sur ce site.",
"media": {
"title": "Graphismes, photos et vidéos",
"body": "Les graphismes, photographies et ressources multimédias sont le plus souvent couverts par des droits de propriété intellectuelle détenus par des tiers et ne sont pas, à ce titre, réutilisables sans leur accord. Aussi, nous vous invitons à nous contacter si vous souhaitez réutiliser de tels contenus afin que nous puissions vous orienter vers le(s) titulaire(s) de ces droits : visio@numerique.gouv.fr."
}
},
"liability": {
"title": "Clause de responsabilité",
"body1": "Les informations proposées sur ce site le sont à titre de service rendu aux agents du service public, notamment aux agents des services de l'Etat.",
"body2": "Les informations et/ou documents disponibles sur ce site sont susceptibles d'être modifiés à tout moment, et peuvent faire l'objet de mises à jour pour faciliter l'accès aux ressources numériques.",
"body3": "Les informations et/ou documents accédés à partir de ce site relèvent de la responsabilité des porteurs de chaque service interministériel ainsi mis à disposition.",
"body4": "L'éditeur ne pourra en aucun cas être tenue responsable de tout dommage de quelque nature qu'il soit résultant de la mauvaise utilisation des ressources accessibles à partir de ce site."
},
"dataProtection": {
"title": "Délégué à la protection des données",
"body": "Si vous souhaitez obtenir des informations sur les traitements de données personnelles gérés par un Service Premier Ministre vous pouvez contacter le délégué à la protection des données (DPD) par courriel à l'adresse dpd@pm.gouv.fr ou par courrier à l'adresse suivante :",
"address": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75700 Paris"
}
"body1": "Sauf mention explicite de propriété intellectuelle détenue par des tiers, les contenus de ce site sont proposés sous ",
"license": "licence ouverte Etalab 2.0",
"body2": "Vous êtes notamment libres de les reproduire, copier, modifier, extraire, transformer, communiquer diffuser, redistribuer, publier, transmettre et exploiter sous réserve de mentionner leur source, leur date de dernière mise à jour et ne pas induire en erreur des tiers quant aux informations qui y figurent.",
"body3": "Tout site public ou privé est autorisé à établir, sans autorisation préalable, un lien (y compris profond) vers les informations diffusées sur ce site."
}
}
+34 -11
View File
@@ -61,6 +61,17 @@
"createRoom": {
"heading": "Authentification requise",
"body": "Cette réunion n'a pas encore été créée. Veuillez vous authentifier pour la créer ou attendre qu'un utilisateur authentifié le fasse."
},
"screenShare": {
"title": "Impossible de partager votre écran",
"ariaLabel": "Impossible de partager votre écran",
"message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.",
"macInstructions": "Accèdez à vos",
"macSystemPreferences": "Préférences système",
"helpLinkText": "Pour en savoir plus, consulter",
"helpLinkLabel": "Problème de présentation",
"closeButton": "Ignorer",
"newTab": "Nouvelle fenêtre"
}
},
"controls": {
@@ -100,7 +111,6 @@
"open": "Masquer l'admin",
"closed": "Afficher l'admin"
},
"support": "Support",
"moreOptions": "Plus d'options",
"reactions": {
"button": "Envoyer une réaction",
@@ -119,26 +129,38 @@
"fullscreen": {
"enter": "Plein écran",
"exit": "Quitter le mode plein écran"
}
},
"support": "Contacter l'aide"
}
},
"effects": {
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
"notAvailable": "Les effets vidéo seront bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome pour une meilleure performance ou Firefox :(",
"activateCamera": "Votre caméra est désactivée. Choisissez une option pour l'activer.",
"notAvailable": "Les effets vidéo seront bientôt disponibles sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome pour de meilleures performances ou Firefox :(",
"heading": "Flou",
"blur": {
"title": "Flou d'arrière-plan",
"light": "Léger flou",
"light": "Flou léger",
"normal": "Flou",
"apply": "Appliquer le flou",
"apply": "Activer le flou",
"clear": "Désactiver le flou"
},
"virtual": {
"title": "Arrière-plan virtuel",
"apply": "Appliquer l'arrière plan virtuel",
"clear": "Désactiver l'arrière plan virtuel"
"apply": "Activer l'arrière-plan virtuel",
"clear": "Désactiver l'arrière-plan virtuel"
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet sur tous les navigateurs et une meilleur qualité."
"faceLandmarks": {
"title": "Effets visuels",
"glasses": {
"apply": "Ajouter des lunettes",
"clear": "Retirer les lunettes"
},
"french": {
"apply": "Ajouter la touche française",
"clear": "Retirer la touche française"
}
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité."
},
"sidePanel": {
"heading": {
@@ -176,11 +198,12 @@
"description": "Ces paramètres organisateur vous permettent de garder le contrôle de votre réunion. Seuls les organisateurs peuvent accéder à ces commandes.",
"access": {
"title": "Accès à la réunion",
"description": "Ces paramètres s'appliqueront également aux futures occurences de cette réunion.",
"description": "Ces paramètres s'appliqueront également aux futures occurrences de cette réunion.",
"type": "Type d'accès à la réunion",
"levels": {
"public": {
"label": "Ouvrir",
"description": "Persone n'a à demander pour rejoindre la réunion."
"description": "Tout le monde peut rejoindre la réunion sans autorisation."
},
"trusted": {
"label": "Ouvrir aux personnes de confiance",
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Créer un lien Visio"
"createMeeting": {
"createButton": "Créer un lien",
"joinButton": "Participer avec Visio",
"copyLinkTooltip": "Copier le lien",
"resetLabel": "Réinitialiser",
"participantLimit": "Jusqu'à 150 participants.",
"popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site."
}
}
+88 -215
View File
@@ -1,221 +1,94 @@
{
"terms": {
"title": "Conditions Générales d'Utilisation",
"introduction": "Les présentes conditions générales d'utilisation (dites \"CGU\") fixent le cadre juridique de la Plateforme \"Visio\" et définissent les conditions d'accès et d'utilisation des services par l'utilisateur.",
"articles": {
"article1": {
"title": "Article 1 - Champ d'application",
"content": "La plateforme \"visio.numerique.gouv.fr\" est à l'initiative de l'opérateur interministériel de la Direction Interministérielle du NUMérique qui en est le responsable de traitements et le porteur opérationnel de la solution."
},
"article2": {
"title": "Article 2 - Objet",
"content": "La plateforme peut collecter des données à caractère personnel pour les finalités suivantes :",
"purposes": [
"Favoriser la coopération des équipes et le travail à distance.",
"Faciliter l'organisation de réunions, conférences ou formations.",
"Améliorer la plateforme Visio."
]
},
"article3": {
"title": "Article 3 - Définitions",
"definitions": [
{
"term": "L'Utilisateur",
"definition": "est tout agent public ou interlocuteur d'agent public, utilisant la plateforme."
},
{
"term": "Les Services",
"definition": "sont les fonctionnalités offertes par la plateforme pour répondre à ses finalités."
},
{
"term": "Le responsable de traitement",
"definition": "est la personne qui, au sens de l'article 4 du règlement UE) n°2016/679 du Parlement européen et du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l'égard du traitement des données à caractère personnel et à la libre circulation de ces données détermine les finalités et les moyens des traitements de données à caractère personnel."
}
]
},
"article4": {
"title": "Article 4 - Cadre et restrictions d'usage",
"content": "Visio est en cours d'homologuation pour un cadre d'usage de données non sensibles, qui exclut le traitement des données personnelles dites « sensibles » relevant des articles 9 et 10 du RGPD et des informations portant la mention \"Diffusion Restreinte\" au sens de l'instruction interministérielle 901/SGDSN/ANSSI."
},
"article5": {
"title": "Article 5 - Fonctionnalités",
"sections": {
"section1": {
"title": "5.1 - Fonctionnalités ouvertes aux agents publics",
"content": "Visio offre à tout agent public la possibilité de créer un salon de visioconférence depuis un poste de travail standard, lié à son compte ProConnect.",
"paragraph1": "Tout agent de l'État, dont les activités sont compatibles avec l'utilisation de Visio ou qui n'est pas soumis à des obligations particulières de confidentialité ou de secret professionnel, peut créer une visioconférence en toute liberté. Il peut ensuite partager le lien de la réunion avec d'autres participants, qu'ils fassent partie de l'administration ou non.",
"paragraph2": "A cet effet, chaque organisateur de salon peut :",
"capabilities": [
"Protéger l'accès au salon par une salle d'attente ;",
"Autoriser l'entrée de chaque participant dans le salon ;",
"Couper les micros et caméras des participants ;",
"Exclure un participant du salon."
],
"paragraph3": "Seuls les agents, dont le domaine d'activité est autorisé à utiliser Visio ou qui ne sont pas soumis à une obligation de confidentialité particulière ou de secret professionnel, peuvent utiliser ou avoir la maîtrise de ce service et de ses fonctionnalités."
},
"section2": {
"title": "5.2 - Fonctionnalités ouvertes à tous les Utilisateurs",
"content": "Visio permet à tout utilisateur, agent public ou non de participer à tout salon de Visio auquel il est convié par un agent de l'Etat et de profiter de toutes ses fonctionnalités.",
"paragraph": "A cet effet, chaque participant peut :",
"capabilities": [
"décliner son identité ou indiquer en entrant dans le salon ;",
"échanger à distance en audio et/ou en vidéo ;",
"partager son écran ;",
"échanger des messages durant le salon de manière publique avec tous ;",
"voir des informations sur la conférence ;",
"exercer son droit à l'image en choisissant de ne jamais activer sa caméra et/ou son micro ou de désactiver l'un, l'autre ou les deux à tout instant ;",
"protéger l'intimité du lieu duquel il accède au salon en sélectionnant puis en activant un fond d'écran avant d'entrer dans le salon ou même durant la visioconférence."
]
}
}
},
"article6": {
"title": "Article 6 - Responsabilité",
"sections": {
"section1": {
"title": "6.1 - L'Editeur de la plateforme",
"paragraphs": [
"L'éditeur s'autorise à suspendre ou révoquer l'accès au service, s'il estime que l'usage réalisé du service porte préjudice à son image ou ne correspond pas aux exigences de sécurité.",
"L'éditeur s'engage à la sécurisation de la Plateforme, notamment en prenant toutes les mesures nécessaires permettant de garantir la sécurité et la confidentialité des informations fournies.",
"L'éditeur fournit les moyens nécessaires et raisonnables pour assurer un accès continu, sans contrepartie financière pour les utilisateurs, à la Plateforme. Il se réserve la liberté de faire évoluer, de modifier ou de suspendre, sans préavis, la plateforme pour des raisons de maintenance ou pour tout autre motif jugé nécessaire."
]
},
"section2": {
"title": "6.2 - L'Utilisateur de la plateforme",
"paragraphs": [
"Toute information transmise par l'Utilisateur est de sa seule responsabilité. Il est rappelé que toute personne procédant à une fausse déclaration pour elle-même ou pour autrui s'expose, notamment, aux sanctions prévues à l'article 441-1 du code pénal, prévoyant des peines pouvant aller jusqu'à trois ans d'emprisonnement et 45 000 euros d'amende.",
"Il revient à l'Utilisateur de s'assurer préalablement d'être autorisé à utiliser Visio ou à ne pas être soumis à une obligation de confidentialité particulière ou secret professionnel."
]
}
}
},
"article7": {
"title": "Article 7 - Protection des données à caractère personnel",
"sections": {
"section1": {
"title": "7.1 - Responsable de traitement",
"content": "Le responsable de traitement est la Direction interministérielle du Numérique, dont la directrice est Madame Stéphanie Schaer."
},
"section2": {
"title": "7.2 - Données personnelles traitées",
"content": "La plateforme peut traiter les données à caractère personnel suivantes :",
"dataCategories": [
{
"category": "Données relatives à l'agent public organisateur :",
"items": [
"adresse e-mail de l'agent public organisateur et des participants ;",
"Organisation ;",
"Nom Prénom ;",
"Adresse IP publique ;"
]
},
{
"category": "Données relatives aux visioconférences ;"
},
{
"category": "Données collectées par l'hébergeur :",
"items": [
"L'adresse IP publique du client (terminal associé au navigateur utilisé)",
"L'adresse IP du serveur",
"Données techniques de la connexion portant sur",
"le Navigateur (type, n° de version)",
"le Système d'exploitation du poste client",
"les flux (bande passante, débit, perte de paquets, latence)",
"la qualité de l'image (résolution, nombre d'images/s) en envoi et en réception."
]
}
]
},
"section3": {
"title": "7.3 - Finalités des traitements",
"content": "La plateforme peut traiter des données à caractère personnel pour les finalités suivantes :",
"purposes": [
"Favoriser la coopération des équipes et le travail à distance ;",
"Faciliter l'organisation de réunions, conférences ou formations ;",
"Faciliter l'assistance à chaque utilisateur du service.",
"Améliorer la plateforme Visio ;"
]
},
"section4": {
"title": "7.4 - Base juridique des traitements de données",
"content": "Les données traitées à l'occasion de ce traitement ont plusieurs fondements juridiques :",
"bases": [
"l'obligation légale à laquelle est soumis le responsable de traitements au sens de l'article 6-c du RGPD ;",
"la mission d'intérêt public à laquelle le responsable du traitement est soumis au sens de l'article 6-e du RGPD."
],
"details": [
{
"title": "a) Les données relatives à l'agent public organisateur",
"content": "Ce traitement de données est nécessaire à l'exercice d'une mission d'intérêt public ou relevant de l'exercice de l'autorité publique dont est investi le responsable de traitement au sens de l'article 6-e du règlement (UE) 2016/679 du Parlement européen et du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l'égard du traitement des données à caractère personnel et à la libre circulation de ces données.",
"legalReference": "La mission d'intérêt public est notamment posée par : L'article 6 du décret n°2019-1088 du 25 octobre 2019 relatif au système d'information et de communication de l'Etat et à la direction interministérielle du numérique"
},
{
"title": "b) Les données relatives à la visioconférence",
"content": "Ce traitement de données est nécessaire à l'exercice d'une mission d'intérêt public ou relevant de l'exercice de l'autorité publique dont est investi le responsable de traitement au sens de l'article 6-e du Règlement (UE) 2016/679 du Parlement européen et du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l'égard du traitement des données à caractère personnel et à la libre circulation de ces données.",
"legalReference": "La mission d'intérêt public est notamment posée par : L'article 6 du décret n°2019-1088 du 25 octobre 2019 relatif au système d'information et de communication de l'Etat et à la direction interministérielle du numérique"
},
{
"title": "c) Les données de connexion",
"content": "Ce traitement de données est nécessaire au respect d'une obligation légale à laquelle le responsable du traitement est soumis au sens de l'article 6-c du règlement (UE) 2016/679 du Parlement européen et du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l'égard du traitement des données à caractère personnel et à la libre circulation de ces données.",
"legalReferences": [
"La loi LCEN n°2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique",
"Les articles 1 et 3 du décret n°2011-219 du 25 février 2011"
]
}
]
},
"section5": {
"title": "7.5 - Durée de conservation des données traitées",
"content": "Les données à caractère personnel sont conservées aux fins d'éventuelles demandes d'assistance ou de traçabilité pour une conférence, d'élaboration de statistiques :",
"dataType": "Type de données",
"retentionPeriod": "Période de rétention",
"retentionTable": [
{
"dataType": "Données relatives à l'agent organisateur",
"retention": "Les données conservées sont anonymisées et sont supprimées au plus tard 12 mois après la date d'expiration de la visioconférence"
},
{
"dataType": "Données relatives à la visioconference",
"retention": "Les données sont supprimées au plus tard 12 mois après la date d'expiration de la visioconference"
}
],
"additionalInfo": "A l'issue de la durée de conservation, les données relatives à l'agent organisateur et les données relatives à la visioconference ne peuvent plus être utilisées par le responsable de traitements dans le cadre de la plateforme. Elles sont détruites au plus tard 12 mois après la date d'expiration de la visioconference."
},
"section6": {
"title": "7.6 - Sécurité et confidentialité",
"content": "Le responsable de traitement s'engage à prendre les mesures techniques et organisationnelles de sécurité nécessaires pour assurer la confidentialité, l'intégrité et protéger l'accès des données."
},
"section7": {
"title": "7.7 - Droits des personnes concernées",
"content": "En vertu de l'article 13 du règlement (UE) n°2016/679 du Parlement européen et du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l'égard du traitement des données à caractère personnel et à la libre circulation de ces données, il est rappelé à chaque utilisateur dont les données sont collectées dans le cadre de l'utilisation ou de la connexion à la plateforme Visio qu'il dispose des droits suivants concernant ses données à caractère personnel :",
"rights": [
"Droit d'information ;",
"Droit d'accès aux données ;",
"Droit de rectification et droit d'effacement des données ;",
"Droit de limitation des données"
],
"exerciseRights": {
"content": "Vous pouvez exercer ces droits en contactant par :",
"methods": [
"voie électronique : visio@numerique.gouv.fr",
"voie postale : Direction interministérielle du Numérique, 20 avenue de Ségur, 75007 Paris."
]
},
"additionalInfo": [
"En raison de l'obligation de sécurité et de confidentialité dans le traitement des données à caractère personnel qui incombe au responsable de traitement, votre demande ne sera traitée que si vous rapportez la preuve de votre identité. Pour vous aider dans votre démarche, vous trouverez ici un modèle de courrier élaboré par la Cnil.",
"Vous avez la possibilité de vous opposer à un traitement de vos données personnelles. Pour vous aider dans votre démarche, vous trouverez ici un modèle de courrier élaboré par la Cnil.",
"Délais de réponse : La responsable de traitement s'engage à répondre à votre demande d'accès, de rectification ou d'opposition ou toute autre demande complémentaire d'informations dans un délai raisonnable qui ne saurait dépasser 1 mois à compter de la réception de votre demande."
]
},
"section8": {
"title": "7.8 - Destinataires",
"paragraphs": [
"Les données collectées sont traitées par les seules personnes juridiquement habilitées à connaître des informations traitées.",
"Il s'agit des agents, salariés ou autre personne pouvant représenter la personne morale titulaire d'une mission de service public qui utilise le service de la plateforme."
]
}
"title": "Modalités d'utilisation de Visio",
"articles": {
"article1": {
"title": "1. Champ d'application",
"content": "Le présent document définit les modalités d'utilisation de Visio, le service de visioconférence de l'administration."
},
"article2": {
"title": "2. Objet de la plateforme",
"content": "Le service permet de favoriser la coopération des équipes et le travail à distance et de faciliter l'organisation de réunions, conférences ou formations.",
"purposes": "Visio est développé et opéré par la DINUM. Toute utilisation du service doit respecter les présentes modalités d'utilisation."
},
"article3": {
"title": "3. Accès au service",
"definition": "Visio est mis à disposition des agents publics connectés par ProConnect aux fins d'administrer un salon de visioconférence. L'agent public connecté peut ensuite transmettre un lien permettant d'accéder au salon à toute personne, sous sa responsabilité."
},
"article4": {
"title": "4. Utilisation",
"content": "L'utilisation du service est libre et gratuite, pour administration un salon de visioconférence, il est nécessaire de se connecter par ProConnect."
},
"article5": {
"title": "5. Fonctionnalités",
"sections": {
"section1": {
"title": "5.1. Fonctionnalités ouvertes aux agents publics",
"content": "Visio offre aux agents publics la possibilité de créer un salon de visioconférence depuis un poste de travail standard, lié à son compte ProConnect.",
"paragraph1": "Tout agent de l'État, dont les activités sont compatibles avec l'utilisation de Visio ou qui n'est pas soumis à des obligations particulières de confidentialité ou de secret professionnel, peut créer une visioconférence. Il peut ensuite partager le lien de la réunion avec d'autres participants, qu'ils fassent partie de l'administration ou non.",
"paragraph2": "A cet effet, chaque organisateur de salon peut :",
"capabilities": [
"Protéger l'accès au salon par une salle d'attente ;",
"Autoriser l'entrée de chaque participant dans le salon ;",
"Couper les micros et caméras des participants ;",
"Exclure un participant du salon."
],
"paragraph3": "Seuls les agents, dont le domaine d'activité est autorisé à utiliser Visio ou qui <u>ne sont pas soumis à une obligation de confidentialité particulière ou de secret professionnel</u>, peuvent utiliser ou avoir la maîtrise de ce service et de ses fonctionnalités."
},
"section2": {
"title": "5.2. Fonctionnalités ouvertes aux utilisateurs et utilisatrices",
"content": "Visio permet à tout utilisateur et tout utilisatrice, agent public ou non de participer à tout salon de Visio auquel il est convié par un agent de l'Etat et de profiter de toutes ses fonctionnalités.",
"paragraph": "A cet effet, chaque participant et participante peut :",
"capabilities": [
"décliner son identité affichée en entrant dans le salon ;",
"échanger à distance en audio et/ou en vidéo ;",
"partager son écran ;",
"échanger des messages durant le salon de manière publique avec tous ;",
"voir des informations sur la conférence ;",
"activer ou désactiver sa caméra et/ou son micro à tout instant ;",
"protéger l'intimité de son poste en sélectionnant puis en activant un fond d'écran avant d'entrer dans le salon ou même durant la visioconférence."
]
}
}
},
"article6": {
"title": "6. Engagements et responsabilités des utilisateurs et utilisatrices",
"sections": {
"section1": {
"title": "6.1 Usages conformes",
"paragraphs": [
"Le service est mis à disposition pour les agents publics par ProConnect.",
"L'utilisateur ou l'utilisatrice est responsable des données ou contenus qu'il ou elle saisit dans le service de messagerie du salon de visioconférence. Il ou elle veille à ne saisir que des messages appropriés."
]
},
"section2": {
"title": "6.2 Usages interdits",
"paragraphs": [
"L'utilisateur ou l'utilisatrice s'engage à ne pas saisir dans le service de messagerie du salon de Visio des contenus ou informations contraires aux dispositions légales et réglementaires en vigueur."
]
}
}
},
"article7": {
"title": "7. Engagements et responsabilités de la DINUM",
"sections": {
"section1": {
"title": "7.1. Sécurité et accès à la plateforme",
"content": "La DINUM s'engage à la sécurisation de Visio, notamment en prenant toutes les mesures nécessaires permettant de garantir la sécurité et la confidentialité des informations fournies.",
"paragraphs": [
"La DINUM s'engage à fournir les moyens nécessaires et raisonnables pour assurer un accès continu à Visio.",
"La DINUM se réserve le droit de faire évoluer, de modifier ou de suspendre, sans préavis, le service pour des raisons de maintenance ou pour tout autre motif jugé nécessaire.",
"La DINUM se réserve le droit de suspendre ou supprimer un compte utilisateur ou utilisatrice du service qui aurait méconnu les présentes modalités d'utilisation, sans préjudice des éventuelles actions en responsabilité pénale et civile qui pourraient être engagées à l'encontre de l'utilisateur ou l'utilisatrice."
]
},
"section2": {
"title": "7.2 Open Source et Licences",
"content": "Le code source de Visio est libre et disponible ici : https://github.com/suitenumerique/meet\nLes contenus proposés par la DINUM sont sous Licence Ouverte, à l'exception des logos et des représentations iconographiques et photographiques pouvant être régis par leurs licences propres."
}
}
},
"article8": {
"title": "8. Évolution des modalités d'utilisation",
"content": "Les termes des présentes modalités d'utilisation peuvent être modifiés ou complétés à tout moment, sans préavis, en fonction des modifications apportées au service, de l'évolution de la législation ou pour tout autre motif jugé nécessaire.\nCes modifications et mises à jour s'imposent à l'utilisateur ou l'utilisatrice qui doit, en conséquence, se référer régulièrement à cette rubrique pour vérifier les modalités en vigueur."
}
}
}
+12 -18
View File
@@ -1,40 +1,34 @@
{
"accessibility": {
"title": "Accessibility",
"introduction": "Visio is committed to making its digital services accessible, in accordance with Article 47 of Law No. 2005-102 of February 11, 2005.",
"introduction": "<i>Visio</i> is committed to making its digital services accessible, in accordance with Article 47 of Law No. 2005-102 of February 11, 2005.",
"declaration": {
"title": "Accessibility Declaration",
"date": "Established on March 5, 2025."
"title": "Accessibility Statement",
"date": "Established on December 4, 2024."
},
"scope": "This accessibility declaration applies to the site visio.numerique.gouv.fr",
"scope": "This accessibility statement applies to the website visio.numerique.gouv.fr",
"complianceStatus": {
"title": "Compliance Status",
"body": "visio.numerique.gouv.fr is not compliant with RGAA 4.1. The site has not yet been audited. However, the team is striving to create a site accessible to all by following RGAA recommendations."
"body": "visio.numerique.gouv.fr is not compliant with RGAA 4.1. The site has not yet been audited. However, the team strives to create a site that is accessible to everyone by following RGAA recommendations."
},
"improvement": {
"title": "Improvement and Contact",
"body": "If you are unable to access content or a service, you can contact the administrator of lasuite.numerique.gouv.fr to be directed to an accessible alternative or obtain the content in another format.",
"body": "If you cannot access content or a service, you can contact the person responsible for lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.",
"contact": {
"email": "Email: visio@numerique.gouv.fr",
"address": "Address: DINUM, 20 avenue de Ségur 75007 Paris"
},
"response": "We try to respond within 2 business days."
"response": "We try to respond within 2 working days."
},
"recourse": {
"title": "Recourse",
"introduction": "This procedure should be used in the following case: you have reported an accessibility issue to the website administrator that prevents you from accessing content or services on the portal, and you have not received a satisfactory response.",
"title": "Means of Recourse",
"introduction": "This procedure is to be used in the following case: you have reported to the website manager an accessibility issue that prevents you from accessing content or one of the portal's services, and you have not received a satisfactory response.",
"options": {
"intro": "You can:",
"option1": "Send a message to the Defender of Rights",
"option2": "Contact the delegate of the Defender of Rights in your region",
"option3": "Send a letter by mail (free, do not put a stamp):"
"option1": "Write a message to the Rights Defender",
"option2": "Contact the Rights Defender's representative in your region",
"option3": "Send a letter by mail (free, no stamp needed): </br> Rights Defender Free response 71120 75342 Paris CEDEX 07"
}
},
"dataProtection": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75007 Paris"
}
}
}
+17 -49
View File
@@ -1,69 +1,37 @@
{
"title": "Legal Notices",
"title": "Legal Notice",
"creator": {
"title": "Publisher",
"body": "The Visio site is published by the interministerial digital directorate (DINUM), hereinafter referred to as \"the publisher\", belonging to the Prime Minister's services.",
"body": "The Visio service is published by the Interministerial Digital Division of the State (DINUM) located at:",
"contact": {
"title": "Contact Information",
"address": "Address: {{address}}",
"phone": "Reception Tel.: {{phone}}",
"siret": "SIRET: {{siret}}",
"siren": "SIREN: {{siren}}"
"address": "20 avenue de Ségur",
"city": "75007 Paris",
"phone": "Reception Tel.: 01.71.21.01.70",
"siret": "SIRET: 12000101100010 (government general secretariat)",
"siren": "SIREN: 120 001 011"
}
},
"director": {
"title": "Publication Director",
"body": "Interministerial Digital Director."
"body": "The publication director is Madame Stéphanie Schaer, Interministerial Digital Director."
},
"hosting": {
"title": "Hosting",
"body": "This website is hosted by Outscale with SecNumCloud certification:",
"address": {
"line1": "Outscale",
"line2": "1, rue Royale",
"line3": "319 Bureaux de la Colline",
"line4": "92210 Saint-Cloud"
}
"body": "The service is hosted by Outscale West-1 CloudGouv SecNumCloud located in France."
},
"accessibility": {
"title": "Accessibility",
"body": "Compliance with digital accessibility standards is a future objective. The site has not yet been audited. However, the team strives to create a site accessible to all by following the RGAA recommendations.",
"status": "Accessibility status: non-compliant",
"reporting": {
"title": "Report a malfunction",
"body1": "If you encounter an accessibility issue preventing you from accessing content or a site feature, please let us know by contacting visio@numerique.gouv.fr.",
"body2": "If you do not receive a prompt response from us, you have the right to send your grievances or a request for referral to the Rights Defender. You have the right to send your grievances or request for referral to the \"Rights Defender\" by phone at 09 69 39 00 00, via the online form https://formulaire.defenseurdesdroits.fr or by free mail (no postage required):",
"address": {
"line1": "Défenseur des droits",
"line2": "Libre réponse 71120",
"line3": "75342 Paris CEDEX 07"
}
}
"body": "Compliance with digital accessibility standards is a future objective. The site has not yet been audited. However, the team strives to create a site accessible to all by following RGAA recommendations.",
"more": "For more information: ",
"link": "link to accessibility page",
"status": "Accessibility status: non-compliant"
},
"reuse": {
"title": "Content Reuse and Links",
"body1": "Unless explicitly mentioned as intellectual property held by third parties, the contents of this site are offered under an open license.",
"body2": "Any public or private site is authorized to establish, without prior authorization, a link (including deep linking) to the information disseminated on this site.",
"media": {
"title": "Graphics, Photos and Videos",
"body": "Graphics, photographs, and multimedia resources are most often covered by intellectual property rights held by third parties and are not, as such, reusable without their consent. Therefore, we invite you to contact us if you wish to reuse such content so that we can direct you to the holder(s) of these rights: visio@numerique.gouv.fr."
}
},
"liability": {
"title": "Liability Clause",
"body1": "The information offered on this site is provided as a service to public service agents, particularly to agents of State services.",
"body2": "The information and/or documents available on this site are subject to change at any time and may be updated to facilitate access to digital resources.",
"body3": "The information and/or documents accessed from this site are the responsibility of the owners of each interministerial service thus made available.",
"body4": "The publisher cannot under any circumstances be held responsible for any damage of any nature whatsoever resulting from the misuse of resources accessible from this site."
},
"dataProtection": {
"title": "Data Protection Officer",
"body": "If you wish to obtain information about personal data processing managed by a Prime Minister's Service, you can contact the data protection officer (DPO) by email at dpd@pm.gouv.fr or by mail at the following address:",
"address": {
"line1": "Service du Premier ministre",
"line2": "A l'attention du délégué à la protection des données (DPD)",
"line3": "56 rue de Varenne",
"line4": "75700 Paris"
}
"body1": "Unless explicitly mentioned of intellectual property held by third parties, the contents of this site are offered under ",
"license": "Etalab 2.0 open license",
"body2": "You are particularly free to reproduce, copy, modify, extract, transform, communicate, distribute, redistribute, publish, transmit and exploit them provided you mention their source, their date of last update and do not mislead third parties regarding the information contained therein.",
"body3": "Any public or private site is authorized to establish, without prior authorization, a link (including deep link) to the information disseminated on this site."
}
}
+97 -16
View File
@@ -34,7 +34,19 @@
"usernameEmpty": "Uw naam kan niet leeg zijn"
},
"cameraDisabled": "Camera is uitgeschakeld.",
"cameraStarting": "Camera wordt ingeschakeld."
"cameraStarting": "Camera wordt ingeschakeld.",
"waiting": {
"title": "Verzoek tot deelname...",
"body": "U kunt deelnemen aan dit gesprek wanneer iemand u toestemming geeft"
},
"denied": {
"title": "U kunt niet deelnemen aan dit gesprek",
"body": "Uw verzoek tot deelname is geweigerd."
},
"timeoutInvite": {
"title": "U kunt niet deelnemen aan dit gesprek",
"body": "Niemand heeft gereageerd op uw verzoek om deel te nemen aan het gesprek"
}
},
"leaveRoomPrompt": "Dat zal u de vergadering doen verlaten.",
"shareDialog": {
@@ -49,6 +61,17 @@
"createRoom": {
"heading": "Verificatie vereist",
"body": "Deze ruimte is nog niet gemaakt. Logt u alstublieft in om hem aan te maken, of wacht tot een ingelogde gebruiker dat doet."
},
"screenShare": {
"title": "Kan uw scherm niet delen",
"ariaLabel": "Kan uw scherm niet delen",
"message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.",
"macInstructions": "Ga naar uw",
"macSystemPreferences": "Systeemvoorkeuren",
"helpLinkText": "Meer informatie, zie",
"helpLinkLabel": "Presentatieprobleem",
"closeButton": "Negeren",
"newTab": "Nieuw venster"
}
},
"controls": {
@@ -84,7 +107,10 @@
"open": "Verberg AI-assistent",
"closed": "Toon AI-assistant"
},
"support": "Ondersteuning",
"admin": {
"open": "Verberg beheerder",
"closed": "Toon beheerder"
},
"moreOptions": "Meer opties",
"reactions": {
"button": "Stuur reactie",
@@ -103,39 +129,53 @@
"fullscreen": {
"enter": "Volledig scherm",
"exit": "Stop volledig scherm stand"
}
},
"support": "Contact ondersteuning"
}
},
"effects": {
"activateCamera": "Uw camera is uitgeschakeld. Kies een optie om hem in te schakelen.",
"notAvailable": "Video-effecten zijn binnenkort beschikbaar in uw browser. We werken hier aan! Ondertussen kunt u Chrome gebruiken voor de beste prestaties, of Firefox :(",
"activateCamera": "Uw camera is uitgeschakeld. Kies een optie om deze in te schakelen.",
"notAvailable": "Video-effecten zijn binnenkort beschikbaar in uw browser. We werken eraan! In de tussentijd kunt u Google Chrome gebruiken voor de beste prestaties of Firefox :(",
"heading": "Vervaging",
"blur": {
"title": "Achtergrond vervagen",
"light": "Licht vervagen",
"normal": "Vervagen",
"apply": "Schakel vervaging in",
"clear": "Schakel vervaging uit"
"title": "Achtergrondvervaging",
"light": "Lichte vervaging",
"normal": "Vervaging",
"apply": "Vervaging inschakelen",
"clear": "Vervaging uitschakelen"
},
"virtual": {
"title": "Virtuele achtergrond",
"apply": "Schakel virtuele achtergrond in",
"clear": "Schakel virtuele achtergrond uit"
"apply": "Virtuele achtergrond inschakelen",
"clear": "Virtuele achtergrond uitschakelen"
},
"experimental": "Experimentele functionaliteit. Een v2 komt er aan met volledige browser-ondersteuning en verbeterde kwaliteit."
"faceLandmarks": {
"title": "Visuele effecten",
"glasses": {
"apply": "Bril toevoegen",
"clear": "Bril verwijderen"
},
"french": {
"apply": "Franse stijl toevoegen",
"clear": "Franse stijl verwijderen"
}
},
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."
},
"sidePanel": {
"heading": {
"participants": "Deelnemers",
"effects": "Effecten",
"chat": "Berichten in de chat",
"transcript": "AI-assistent"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"content": {
"participants": "deelnemers",
"effects": "effecten",
"chat": "berichten",
"transcript": "AI-assistent"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"closeButton": "Verberg {{content}}"
},
@@ -154,6 +194,28 @@
"button": "Stop met opname"
}
},
"admin": {
"description": "Deze organisatorinstellingen geven u controle over uw vergadering. Alleen organisatoren hebben toegang tot deze bedieningselementen.",
"access": {
"title": "Toegang tot vergadering",
"description": "Deze instellingen zijn ook van toepassing op toekomstige sessies van deze vergadering.",
"type": "Vergaderings toegangstypen",
"levels": {
"public": {
"label": "Open",
"description": "Niemand hoeft toestemming te vragen om deel te nemen aan de vergadering."
},
"trusted": {
"label": "Open voor vertrouwde personen",
"description": "Geauthenticeerde personen hoeven geen toestemming te vragen om deel te nemen aan de vergadering."
},
"restricted": {
"label": "Beperkt",
"description": "Personen die niet zijn uitgenodigd voor de vergadering moeten vragen om deel te nemen."
}
}
}
},
"rating": {
"submit": "Indienen",
"question": "Wat vindt u van de kwaliteit van het gesprek?",
@@ -172,6 +234,12 @@
"heading": "Bedankt voor uw inzending",
"body": "Ons productteam neemt de tijd om uw feedback zorgvuldig te bekijken. We zullen zo snel mogelijk antwoorden."
},
"authenticationMessage": {
"heading": "Hoe kunnen we contact met u opnemen?",
"placeholder": "Uw e-mail",
"submit": "Verzenden",
"ignore": "Negeren"
},
"participants": {
"subheading": "In de ruimte",
"you": "U",
@@ -190,7 +258,20 @@
},
"raisedHands": "Opgestoken handen",
"lowerParticipantHand": "Laat {{name}}'s hand zakken",
"lowerParticipantsHand": "Laat alle handen zakken"
"lowerParticipantsHand": "Laat alle handen zakken",
"waiting": {
"title": "Wachtkamer",
"accept": {
"button": "Accepteren",
"label": "{{name}} toelaten tot de vergadering",
"all": "Alles accepteren"
},
"deny": {
"button": "Weigeren",
"label": "{{name}} weigeren voor de vergadering",
"all": "Alles weigeren"
}
}
},
"recording": {
"label": "Opnemen"
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Maak een Visio link"
"createMeeting": {
"createButton": "Aanmaken",
"joinButton": "Deelnemen met Visio",
"copyLinkTooltip": "Link kopiëren",
"resetLabel": "Resetten",
"participantLimit": "Tot 150 deelnemers.",
"popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site."
}
}
+88 -215
View File
@@ -1,221 +1,94 @@
{
"terms": {
"title": "Terms of Service",
"introduction": "These terms of service (referred to as \"ToS\") establish the legal framework of the \"Visio\" Platform and define the conditions of access and use of services by the user.",
"articles": {
"article1": {
"title": "Article 1 - Scope",
"content": "The platform \"visio.numerique.gouv.fr\" is an initiative of the interministerial operator of the Interministerial Digital Direction, which is responsible for processing and the operational carrier of the solution."
},
"article2": {
"title": "Article 2 - Purpose",
"content": "The platform may collect personal data for the following purposes:",
"purposes": [
"Promote team cooperation and remote work.",
"Facilitate the organization of meetings, conferences or training sessions.",
"Improve the Visio platform."
]
},
"article3": {
"title": "Article 3 - Definitions",
"definitions": [
{
"term": "User",
"definition": "Any public agent or interlocutor of a public agent, using the platform."
},
{
"term": "Services",
"definition": "The functionalities offered by the platform to meet its purposes."
},
{
"term": "Data controller",
"definition": "The person who, within the meaning of Article 4 of Regulation (EU) No. 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, determines the purposes and means of processing personal data."
}
]
},
"article4": {
"title": "Article 4 - Framework and usage restrictions",
"content": "Visio is in the process of certification for a non-sensitive data usage framework, which excludes the processing of so-called \"sensitive\" personal data under Articles 9 and 10 of the GDPR and information bearing the mention \"Restricted Distribution\" within the meaning of interministerial instruction 901/SGDSN/ANSSI."
},
"article5": {
"title": "Article 5 - Features",
"sections": {
"section1": {
"title": "5.1 - Features open to public agents",
"content": "Visio offers any public agent the possibility to create a videoconference room from a standard workstation, linked to their ProConnect account.",
"paragraph1": "Any State agent, whose activities are compatible with the use of Visio or who is not subject to particular confidentiality obligations or professional secrecy, can create a videoconference freely. They can then share the meeting link with other participants, whether they are part of the administration or not.",
"paragraph2": "For this purpose, each room organizer can:",
"capabilities": [
"Protect access to the room with a waiting room;",
"Authorize the entry of each participant in the room;",
"Mute participants' microphones and cameras;",
"Exclude a participant from the room."
],
"paragraph3": "Only agents whose field of activity is authorized to use Visio or who are not subject to a particular confidentiality obligation or professional secrecy can use or control this service and its functionalities."
},
"section2": {
"title": "5.2 - Features open to all Users",
"content": "Visio allows any user, public agent or not, to participate in any Visio room to which they are invited by a State agent and to benefit from all its features.",
"paragraph": "For this purpose, each participant can:",
"capabilities": [
"Decline their identity or indicate it when entering the room;",
"Exchange remotely in audio and/or video;",
"Share their screen;",
"Exchange messages during the room publicly with everyone;",
"See information about the conference;",
"Exercise their right to their image by choosing never to activate their camera and/or microphone or to deactivate one, the other or both at any time;",
"Protect the privacy of the place from which they access the room by selecting and then activating a screen background before entering the room or even during the videoconference."
]
}
}
},
"article6": {
"title": "Article 6 - Responsibility",
"sections": {
"section1": {
"title": "6.1 - The Platform Editor",
"paragraphs": [
"The editor reserves the right to suspend or revoke access to the service if it considers that the use of the service is detrimental to its image or does not correspond to security requirements.",
"The editor is committed to securing the Platform, particularly by taking all necessary measures to guarantee the security and confidentiality of the information provided.",
"The editor provides the necessary and reasonable means to ensure continuous access, without financial counterpart for users, to the Platform. It reserves the freedom to evolve, modify or suspend, without notice, the platform for maintenance reasons or for any other reason deemed necessary."
]
},
"section2": {
"title": "6.2 - The Platform User",
"paragraphs": [
"Any information transmitted by the User is their sole responsibility. It is reminded that any person making a false declaration for themselves or for others is exposed, in particular, to the sanctions provided for in Article 441-1 of the Penal Code, providing for penalties of up to three years' imprisonment and a fine of 45,000 euros.",
"It is up to the User to ensure beforehand that they are authorized to use Visio or not to be subject to a particular confidentiality obligation or professional secrecy."
]
}
}
},
"article7": {
"title": "Article 7 - Protection of personal data",
"sections": {
"section1": {
"title": "7.1 - Data controller",
"content": "The data controller is the Interministerial Digital Direction, whose director is Ms. Stéphanie Schaer."
},
"section2": {
"title": "7.2 - Personal data processed",
"content": "The platform may process the following personal data:",
"dataCategories": [
{
"category": "Data relating to the organizing public agent:",
"items": [
"Email address of the organizing public agent and participants;",
"Organization;",
"First and last name;",
"Public IP address;"
]
},
{
"category": "Data relating to videoconferences;"
},
{
"category": "Data collected by the host:",
"items": [
"The public IP address of the client (terminal associated with the browser used)",
"The IP address of the server",
"Technical connection data relating to",
"the Browser (type, version no.)",
"the Client workstation operating system",
"the flows (bandwidth, throughput, packet loss, latency)",
"the image quality (resolution, number of frames/s) in sending and receiving."
]
}
]
},
"section3": {
"title": "7.3 - Purposes of processing",
"content": "The platform may process personal data for the following purposes:",
"purposes": [
"Promote team cooperation and remote work;",
"Facilitate the organization of meetings, conferences or training sessions;",
"Facilitate assistance to each user of the service.",
"Improve the Visio platform;"
]
},
"section4": {
"title": "7.4 - Legal basis for data processing",
"content": "The data processed on the occasion of this processing have several legal bases:",
"bases": [
"the legal obligation to which the data controller is subject within the meaning of Article 6-c of the GDPR;",
"the public interest mission to which the data controller is subject within the meaning of Article 6-e of the GDPR."
],
"details": [
{
"title": "a) Data relating to the organizing public agent",
"content": "This data processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller within the meaning of Article 6-e of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReference": "The public interest mission is notably established by: Article 6 of Decree No. 2019-1088 of October 25, 2019 relating to the State's information and communication system and to the interministerial digital direction"
},
{
"title": "b) Data relating to the web conference",
"content": "This data processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller within the meaning of Article 6-e of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReference": "The public interest mission is notably established by: Article 6 of Decree No. 2019-1088 of October 25, 2019 relating to the State's information and communication system and to the interministerial digital direction"
},
{
"title": "c) Connection data",
"content": "This data processing is necessary for compliance with a legal obligation to which the controller is subject within the meaning of Article 6-c of Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data.",
"legalReferences": [
"The LCEN Law No. 2004-575 of June 21, 2004 for confidence in the digital economy",
"Articles 1 and 3 of Decree No. 2011-219 of February 25, 2011"
]
}
]
},
"section5": {
"title": "7.5 - Retention period of processed data",
"content": "Personal data are kept for the purposes of possible requests for assistance or traceability for a conference, for the development of statistics:",
"dataType": "Data Type",
"retentionPeriod": "Retention Period",
"retentionTable": [
{
"dataType": "Data relating to the organizing agent",
"retention": "The data kept are anonymized and are deleted at the latest 12 months after the expiry date of the videoconference"
},
{
"dataType": "Data relating to the videoconference",
"retention": "The data are deleted at the latest 12 months after the expiry date of the videoconference"
}
],
"additionalInfo": "At the end of the retention period, the data relating to the organizing agent and the data relating to the videoconference can no longer be used by the data controller within the framework of the platform. They are destroyed at the latest 12 months after the expiry date of the videoconference."
},
"section6": {
"title": "7.6 - Security and confidentiality",
"content": "The data controller undertakes to take the necessary technical and organizational security measures to ensure confidentiality, integrity and protect data access."
},
"section7": {
"title": "7.7 - Rights of data subjects",
"content": "Pursuant to Article 13 of Regulation (EU) No. 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, each user whose data is collected in the context of the use or connection to the Visio platform is reminded that they have the following rights regarding their personal data:",
"rights": [
"Right to information;",
"Right of access to data;",
"Right of rectification and right of erasure of data;",
"Right to restriction of data"
],
"exerciseRights": {
"content": "You can exercise these rights by contacting by:",
"methods": [
"Electronic means: visio@numerique.gouv.fr",
"Postal means: Direction interministérielle du Numérique, 20 avenue de Ségur, 75007 Paris."
]
},
"additionalInfo": [
"Due to the obligation of security and confidentiality in the processing of personal data incumbent on the data controller, your request will only be processed if you provide proof of your identity. To help you in your approach, you will find here a model letter developed by the Cnil.",
"You have the possibility to object to the processing of your personal data. To help you in your approach, you will find here a model letter developed by the Cnil.",
"Response times: The data controller undertakes to respond to your request for access, rectification or objection or any other request for additional information within a reasonable time that should not exceed 1 month from the receipt of your request."
]
},
"section8": {
"title": "7.8 - Recipients",
"paragraphs": [
"The collected data are processed only by persons legally authorized to know the processed information.",
"These are agents, employees or other persons who may represent the legal entity holding a public service mission that uses the platform service."
]
}
"title": "Terms of Use for Visio",
"articles": {
"article1": {
"title": "1. Scope",
"content": "This document defines the terms of use for Visio, the videoconferencing service of the administration."
},
"article2": {
"title": "2. Purpose of the Platform",
"content": "The service promotes team cooperation and remote work, and facilitates the organization of meetings, conferences, or training sessions.",
"purposes": "Visio is developed and operated by DINUM. Any use of the service must comply with these terms of use."
},
"article3": {
"title": "3. Access to the Service",
"definition": "Visio is made available to public agents connected through ProConnect for the purpose of administering a videoconference room. The connected public agent can then transmit a link allowing access to the room to any person, under their responsibility."
},
"article4": {
"title": "4. Usage",
"content": "The use of the service is free of charge. To administer a videoconference room, it is necessary to connect through ProConnect."
},
"article5": {
"title": "5. Features",
"sections": {
"section1": {
"title": "5.1. Features Available to Public Agents",
"content": "Visio offers public agents the ability to create a videoconference room from a standard workstation, linked to their ProConnect account.",
"paragraph1": "Any State agent whose activities are compatible with the use of Visio or who is not subject to specific confidentiality obligations or professional secrecy, can create a videoconference. They can then share the meeting link with other participants, whether they are part of the administration or not.",
"paragraph2": "To this end, each room organizer can:",
"capabilities": [
"Protect access to the room with a waiting room;",
"Authorize the entry of each participant into the room;",
"Mute the microphones and cameras of participants;",
"Remove a participant from the room."
],
"paragraph3": "Only agents whose field of activity is authorized to use Visio or who <u>are not subject to specific confidentiality obligations or professional secrecy</u>, may use or have control of this service and its features."
},
"section2": {
"title": "5.2. Features Available to Users",
"content": "Visio allows any user, whether a public agent or not, to participate in any Visio room to which they are invited by a State agent and to benefit from all its features.",
"paragraph": "To this end, each participant can:",
"capabilities": [
"Set their displayed identity when entering the room;",
"Communicate remotely via audio and/or video;",
"Share their screen;",
"Exchange messages during the session publicly with everyone;",
"View information about the conference;",
"Enable or disable their camera and/or microphone at any time;",
"Protect the privacy of their workstation by selecting and activating a background before entering the room or even during the videoconference."
]
}
}
},
"article6": {
"title": "6. User Commitments and Responsibilities",
"sections": {
"section1": {
"title": "6.1 Compliant Uses",
"paragraphs": [
"The service is made available to public agents through ProConnect.",
"The user is responsible for the data or content they enter in the videoconference room's messaging service. They must ensure they only enter appropriate messages."
]
},
"section2": {
"title": "6.2 Prohibited Uses",
"paragraphs": [
"The user agrees not to enter content or information in the Visio room's messaging service that is contrary to current legal and regulatory provisions."
]
}
}
},
"article7": {
"title": "7. DINUM Commitments and Responsibilities",
"sections": {
"section1": {
"title": "7.1. Security and Platform Access",
"content": "DINUM is committed to securing Visio, notably by taking all necessary measures to ensure the security and confidentiality of the information provided.",
"paragraphs": [
"DINUM commits to providing the necessary and reasonable means to ensure continuous access to Visio.",
"DINUM reserves the right to evolve, modify, or suspend the service without notice for maintenance reasons or for any other reason deemed necessary.",
"DINUM reserves the right to suspend or delete a user account of the service that has violated these terms of use, without prejudice to any potential criminal and civil liability actions that could be taken against the user."
]
},
"section2": {
"title": "7.2 Open Source and Licenses",
"content": "The source code of Visio is free and available here: https://github.com/suitenumerique/meet\nThe content offered by DINUM is under an Open License, with the exception of logos and iconographic and photographic representations that may be governed by their own licenses."
}
}
},
"article8": {
"title": "8. Evolution of Terms of Use",
"content": "The terms of these terms of use may be modified or supplemented at any time, without notice, depending on changes made to the service, changes in legislation, or for any other reason deemed necessary.\nThese modifications and updates are binding on the user who must, consequently, regularly refer to this section to verify the terms in force."
}
}
}
+7 -1
View File
@@ -75,7 +75,13 @@ export const A = ({
return (
<Link
{...props}
className={link({ size, externalIcon, underline, footer, color })}
className={link({
size,
externalIcon,
underline,
footer,
color,
})}
/>
)
}
+3 -1
View File
@@ -44,7 +44,7 @@ const FieldWrapper = styled('div', {
const StyledLabel = styled(Label, {
base: {
display: 'block',
fontSize: '0.75rem',
fontSize: '0.875rem',
},
variants: {
center: {
@@ -218,8 +218,10 @@ export const Field = <T extends object>({
{item.description && (
<Text
variant="note"
wrap={'pretty'}
className={css({
textStyle: 'sm',
marginBottom: '0.5rem',
})}
>
{item.description}
+4
View File
@@ -53,6 +53,10 @@ export const text = cva({
note: {
color: 'default.subtle-text',
},
smNote: {
color: 'default.subtle-text',
textStyle: 'sm',
},
inherits: {},
},
centered: {
+15 -1
View File
@@ -3,6 +3,8 @@ import { HomeRoute } from '@/features/home'
import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute'
import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService'
import { CreatePopup } from '@/features/sdk/routes/CreatePopup'
import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton'
export const routes: Record<
| 'home'
@@ -10,7 +12,9 @@ export const routes: Record<
| 'feedback'
| 'legalTerms'
| 'accessibility'
| 'termsOfService',
| 'termsOfService'
| 'sdkCreatePopup'
| 'sdkCreateButton',
{
name: RouteName
path: RegExp | string
@@ -50,6 +54,16 @@ export const routes: Record<
path: '/conditions-utilisation',
Component: TermsOfServiceRoute,
},
sdkCreatePopup: {
name: 'sdkCreatePopup',
path: '/sdk/create-popup',
Component: CreatePopup,
},
sdkCreateButton: {
name: 'sdkCreateButton',
path: '/sdk/create-button',
Component: CreateMeetingButton,
},
}
export type RouteName = keyof typeof routes
+40
View File
@@ -30,6 +30,46 @@
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/fonts/Marianne-Regular-subset.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/fonts/Marianne-Regular_Italic-subset.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/fonts/Marianne-Medium-subset.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/fonts/Marianne-Bold-subset.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/fonts/Marianne-ExtraBold-subset.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
/*
* to reduce CLS
* values taken from https://github.com/khempenius/font-fallbacks-dataset/blob/main/font-metric-overrides.csv#L2979
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.15",
"version": "0.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.15",
"version": "0.1.17",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.15",
"version": "0.1.17",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+1 -1
View File
@@ -24,6 +24,6 @@
"globals": "15.14.0",
"typescript": "5.7.3",
"typescript-eslint": "8.18.2",
"vite": "6.0.11"
"vite": "6.2.5"
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ function App() {
</div>
<div className="group">
<label>Visioconference</label>
<VisioCreateButton onRoomCreated={setRoomUrl} />
<VisioCreateButton
onRoomCreated={(data) => setRoomUrl(data.url)}
onClear={() => setRoomUrl("")}
/>
</div>
<div className="group">
<label>Description</label>
+53
View File
@@ -72,3 +72,56 @@ button {
font-weight: 300;
cursor: pointer;
}
.create-meeting {
background-color: rgb(45, 45, 70);
color: white;
height: 46px;
border-radius: 4px;
font-size: 16px;
font-weight: 300;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 0.75rem;
border: none;
}
.create-meeting svg path {
fill: white !important;
}
.icon {
color: white !important;
}
.join-button {
background-color: rgb(45, 45, 70);
color: white;
height: 46px;
border-radius: 4px;
font-size: 16px;
font-weight: 300;
cursor: pointer;
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
width: fit-content;
padding: 0 1rem;
text-decoration: none;
}
.join-button svg path {
fill: white !important;
}
.join-link {
padding-top: 0.5rem;
}
.join-link svg path {
fill: white !important;
}

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