Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cffd658825 | |||
| 3b1b644ef9 | |||
| ab7a0e1ec2 | |||
| 86a4d68daa | |||
| de3df889ba | |||
| 7c9fc15359 | |||
| a6a67f2752 | |||
| 00eb52cd3f | |||
| 6324be9fd3 | |||
| 66f307b7e8 | |||
| 506b3978e1 | |||
| db65aef56e | |||
| 4e1a4be650 | |||
| 1f603ce17b | |||
| 672ca7dfe4 | |||
| 7405011cd2 | |||
| 1d5aebcfdc | |||
| 0d6881382b | |||
| c428f39456 | |||
| cd9b80b966 | |||
| f80603b4bb | |||
| 3299ae7c39 | |||
| 93ca4f2bf4 | |||
| 19a4e442c5 | |||
| 53f1c9c1d4 | |||
| 28538b63da | |||
| 94ae5d52c2 | |||
| 6b38a3c996 | |||
| 0af30ec366 | |||
| 33e017464f | |||
| 9e91bbe417 | |||
| e821982353 | |||
| 18f4a117ab | |||
| b5b99d4c52 | |||
| 8a4323aa82 | |||
| 5cbac23f13 | |||
| d55810784d | |||
| c2fdefe7b2 | |||
| bf2474d0e1 | |||
| 558d844ef3 | |||
| 94aa828d87 | |||
| 110e062e56 | |||
| f7c2beb30d | |||
| 5bf1456200 | |||
| 80e5c9bb95 | |||
| 74164b8498 | |||
| 82cc2e5d17 | |||
| fa1503db0d | |||
| e106415740 | |||
| 4199328a4a | |||
| 57715fea73 |
@@ -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 }}"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "secrets"]
|
||||
path = secrets
|
||||
url = ../secrets
|
||||
|
||||
@@ -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
|
||||
@@ -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 l’adapter, la modifier, l’extraire et la transformer, notamment pour créer des « Informations dérivées » ;
|
||||
- de la diffuser, la redistribuer, la publier et la transmettre, de l’exploiter à titre commercial, par exemple en la combinant avec d’autres informations, ou en l’incluant 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 l’adresse (URL) renvoyant vers « l’Information » et assurant une mention effective de sa paternité.
|
||||
|
||||
**Par exemple :**
|
||||
|
||||
Dans le cas d’une réutilisation de la base SIRENE de l’INSEE, mentionner l’URL du « Concédant » : www.insee.fr + la date de dernière mise à jour de l’Information 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 l’objet d’une « 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 n’est pas prévue par la présente licence. L’absence de défauts ou d’erreurs éventuellement contenues dans l’ «Information», comme la fourniture continue de l’ « Information » n’est 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 qu’avec 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 l’Open 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 l’article 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** » : l’utilisation de l’« Information » à d’autres 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 d’une combinaison de l’ « Information » et d’autres 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 d’auteur, droits voisins au droit d’auteur, 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 d’un cadre juridique global visant à une diffusion spontanée par les administrations de leurs informations publiques afin d’en 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 l’administration (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 l’administration en vertu du décret pris en application de l’article L.323-2 du CRPA.
|
||||
|
||||
Etalab est la mission chargée, sous l’autorité du Premier ministre, d’ouvrir 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 l’article 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 s’ils le souhaitent.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
We’re 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
|
||||
|
||||
We’re 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.
|
||||
We’re 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. We’re committed to making Visio as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
|
||||
Our users come first. We’re 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,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
@@ -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 |
|
||||
|
||||
-1
Submodule secrets deleted from 2ba12db71d
@@ -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")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+1274
-2069
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
+27
@@ -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
|
||||
|
||||
+103
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ const Marianne = () => {
|
||||
className={css({
|
||||
letterSpacing: '-.01em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'Marianne',
|
||||
fontSize: '1.25rem',
|
||||
lineHeight: '1.75rem',
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"createButton": {
|
||||
"label": ""
|
||||
"createMeeting": {
|
||||
"createButton": "",
|
||||
"joinButton": "",
|
||||
"copyLinkTooltip": "",
|
||||
"resetLabel": "",
|
||||
"participantLimit": "",
|
||||
"popupBlocked": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,13 @@ export const A = ({
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={link({ size, externalIcon, underline, footer, color })}
|
||||
className={link({
|
||||
size,
|
||||
externalIcon,
|
||||
underline,
|
||||
footer,
|
||||
color,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -53,6 +53,10 @@ export const text = cva({
|
||||
note: {
|
||||
color: 'default.subtle-text',
|
||||
},
|
||||
smNote: {
|
||||
color: 'default.subtle-text',
|
||||
textStyle: 'sm',
|
||||
},
|
||||
inherits: {},
|
||||
},
|
||||
centered: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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,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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user