Compare commits

..

11 Commits

Author SHA1 Message Date
lebaudantoine 79e7831a85 🚨(ci) fix the shellcheck job
Get the shellcheck CI job to pass again by addressing the issues it
flagged across our shell scripts.

Note: I am not 100% sure of every fix applied here. Reviewers should
feel free to challenge specific changes and suggest better ones
where relevant.
2026-09-28 18:41:12 +02:00
lebaudantoine bfab9c3ebe 🚨(ci) fix the spellcheck job
Get the spellcheck CI job to pass again by:

* Fixing the actual spelling issues it caught in the project.
* Excluding generated files from the scan, since they are not
  written by us.
* Excluding translation files, whose content is not necessarily in
  English and would trigger false positives.
2026-09-28 18:41:12 +02:00
lebaudantoine a42c915026 👷(ci) add Menshen scan for GitHub Actions vulnerabilities
Wire Menshen into the CI to scan the GitHub Actions we use and flag
vulnerable ones, following the same approach as other projects that
recently adopted it.

Note: I am not fully sure about the current setup. Reviewers should
feel free to adjust the configuration or the integration point as
they see fit.
2026-09-28 18:41:12 +02:00
lebaudantoine fee3060d52 🔥(ci) drop unused Crowdin workflows
The Crowdin workflows were not used by the project and had turned
into dead CI code.

Remove them to reduce noise and keep the CI configuration limited
to what is actually running.
2026-09-28 18:41:12 +02:00
lebaudantoine 07850516d0 👷(ci) migrate CI to the shared workflows repository
First iteration of a migration toward a centralized repository
containing our shared CI logic.

Goals:

* Manage GitHub Actions version upgrades in one place.
* Make it easier to audit what actually runs in CI from a security
  perspective.
* Avoid duplicating CI logic across projects and having each
  repository slowly diverge over time.
* Centralize as many of our custom GitHub Actions as possible,
  including some that still live in the old `numerique-gouv`
  organization.
* Centralize the Renovate configuration alongside the workflows.

Inspired by the Accounts project, which recently simplified and
reorganized its CI setup.

This PR starts moving meet's CI to the shared repository so we can
validate the approach on a real project. For now, reusable
workflows are pinned to `main`; once we agree on the structure and
content of the central repository, they should be pinned to a
specific commit SHA instead.
2026-09-28 18:41:12 +02:00
lebaudantoine 72cd5a8f18 🔥(github) remove local GitHub PR and issue templates
The organization now provides default GitHub templates for pull
requests and issues at the org level, so individual repositories no
longer need to duplicate the same Markdown files.

Delete the local templates so this project uses the organization
defaults, reducing the amount of code we maintain and centralizing
the practice across repositories.
2026-09-28 17:03:41 +02:00
lebaudantoine 21dc63b8ce 🔖(patch) bump release to 1.32.1 2026-09-25 16:47:35 +02:00
lebaudantoine 8d5d42cfdb 🔒️(agent) fix CRITICAL CVE-2026-63072 / CVE-2026-63073 in libssl3t64
Address the following CVEs reported by Trivy on the LiveKit agent
image against `libssl3t64` 3.5.7-1~deb13u2:

* CVE-2026-63073 — CRITICAL (CVSS 9.8)
* CVE-2026-63072

Bump `libssl3t64` to the patched version to pick up both fixes.
2026-09-25 16:40:39 +02:00
lebaudantoine 2480a76b62 🔒️(backend) fix CVE-2026-73228 and CVE-2026-73229 in drf
Address the following MEDIUM severity CVEs reported against
`djangorestframework` 3.17.1:

* CVE-2026-73228 (CVSS 5.3)
* CVE-2026-73229 (CVSS 4.3)

Bump `djangorestframework` to the patched version to pick up the
fixes.
2026-09-25 16:40:39 +02:00
lebaudantoine 31c8f3ec06 🔖(minor) bump release to 1.32.0 2026-09-25 15:18:15 +02:00
snyk-bot a8c4aee0c2 ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.2
Snyk has created this PR to upgrade i18next from 26.4.1 to 26.4.2.

See this package in npm:
i18next

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/af693e79-8c43-4c09-ab65-60580515c9e8?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-09-25 11:56:59 +02:00
65 changed files with 246 additions and 1286 deletions
+9
View File
@@ -0,0 +1,9 @@
[codespell]
# Files that are not English, or generated
skip = ./.git,*.pdf,*.po,*.pot,*.json,*.lock,package-lock.json,
./LICENSES,
./src/summary/summary/core/locales,
./src/summary/summary/core/prompt.py
# Valid words in French (connexion) or in the code (statics)
ignore-words-list = connexion,statics
check-filenames = true
-28
View File
@@ -1,28 +0,0 @@
---
name: 🐛 Bug Report
about: If something is not working as expected 🤔.
---
## Bug Report
**Problematic behavior**
A clear and concise description of the behavior.
**Expected behavior/code**
A clear and concise description of what you expected to happen (or code).
**Steps to Reproduce**
1. Do this...
2. Then this...
3. And then the bug happens!
**Environment**
- Meet version:
- Platform:
**Possible Solution**
<!--- Only if you have suggestions on a fix for the bug -->
**Additional context/Screenshots**
Add any other context about the problem here. If applicable, add screenshots to help explain.
-23
View File
@@ -1,23 +0,0 @@
---
name: ✨ Feature Request
about: I have a suggestion (and may want to build it 💪)!
---
## Feature Request
**Is your feature request related to a problem or unsupported use case? Please describe.**
A clear and concise description of what the problem is. For example: I need to do some task and I have an issue...
**Describe the solution you'd like**
A clear and concise description of what you want to happen. Add any considered drawbacks.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Discovery, Documentation, Adoption, Migration Strategy**
If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
Maybe a screenshot or design?
**Do you want to work on it through a Pull Request?**
<!-- Make sure to coordinate with us before you spend too much time working on an implementation! -->
@@ -1,22 +0,0 @@
---
name: 🤗 Support Question
about: If you have a question 💬, or something was not clear from the docs!
---
<!-- ^ Click "Preview" for a nicer view! ^
We primarily use GitHub as an issue tracker. If however you're encountering an issue not covered in the docs, we may be able to help! -->
---
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/meet).
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/meet/issues).
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
**Topic**
What's the general area of your question: for example, docker setup, database schema, search functionality,...
**Question**
Try to be as specific as possible so we can help you as best we can. Please be patient 🙏
-11
View File
@@ -1,11 +0,0 @@
## Purpose
Description...
## Proposal
Description...
- [] item 1...
- [] item 2...
+19
View File
@@ -0,0 +1,19 @@
name: Changelog Workflow
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
changelog:
uses: suitenumerique/ci/.github/workflows/_changelog.yml@main
+22 -176
View File
@@ -11,180 +11,30 @@ permissions:
contents: read
jobs:
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
- name: show
run: git log
- name: Enforce absence of print statements in code
if: always()
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/workflows/**' | grep "print("
- name: Check absence of fixup commits
if: always()
run: |
! git log | grep 'fixup!'
- name: Install uv
if: always()
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Lint commit messages added to main
if: always()
run: uvx --no-build --from gitlint-core==0.19.1 gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog:
runs-on: ubuntu-latest
if: |
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
github.event_name == 'pull_request'
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
lint-changelog:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
if [ $max_line_length -ge 80 ]; then
echo "ERROR: CHANGELOG has lines longer than 80 characters."
exit 1
fi
build-mails:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22"
- name: Restore the mail templates
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g --ignore-scripts yarn@1.22.22
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
lint-back:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
- name: Lint code with pylint
run: uv run --no-sync --no-build pylint meet demo core
lint-agents:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/agents
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras --no-build
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
lint-summary:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/summary
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
lint-python:
name: lint ${{ matrix.service }}
strategy:
fail-fast: false
matrix:
include:
- service: backend
working_directory: src/backend
pylint_targets: meet demo core
- service: agents
working_directory: src/agents
pylint_targets: ""
- service: summary
working_directory: src/summary
pylint_targets: ""
uses: suitenumerique/ci/.github/workflows/_python-lint.yml@main
with:
working_directory: ${{ matrix.working_directory }}
python_version: "3.13"
pylint_targets: ${{ matrix.pylint_targets }}
test-back:
runs-on: ubuntu-latest
needs: build-mails
permissions:
contents: read
defaults:
@@ -243,12 +93,8 @@ jobs:
sudo mkdir -p /data/media && \
sudo mkdir -p /data/static
- name: Restore the mail templates
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Build or restore the mail templates
uses: suitenumerique/ci/actions/mail-templates@main
- name: Start MinIO
run: |
+18
View File
@@ -0,0 +1,18 @@
name: Project quality Workflow
on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
quality:
uses: suitenumerique/ci/.github/workflows/_project-quality.yml@main
with:
print_check_paths: src/backend src/summary src/agents
codespell_ignore_words: ""
-33
View File
@@ -1,33 +0,0 @@
name: Download Crowdin translations
on:
workflow_dispatch:
types: [file-fully-translated]
permissions:
contents: write
pull-requests: write
jobs:
crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Download Crowdin files
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
with:
upload_sources: false
upload_translations: false
download_translations: true
localization_branch_name: l10n_crowdin_translations
create_pull_request: true
pull_request_title: "New Crowdin translations"
pull_request_body: "New Crowdin pull request with translations"
pull_request_base_branch_name: "main"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
CROWDIN_BASE_PATH: ${{ github.workspace }}
+48 -252
View File
@@ -1,5 +1,5 @@
name: Docker Hub Workflow
run-name: Docker Hub Workflow
name: Docker images
run-name: Docker images
on:
workflow_dispatch:
@@ -15,265 +15,61 @@ on:
permissions:
contents: read
env:
DOCKER_USER: 1001:127
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
IS_MULTI_PLATFORM_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') }}
BUILD_PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
jobs:
build-and-push-backend:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
target: backend-production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend-generic:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend-dinum:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
with:
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./docker/dinum-frontend/Dockerfile
target: frontend-production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-summary:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
continue-on-error: true
with:
docker-build-args: '-f src/summary/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
docker-context: './src/summary'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ./src/summary
file: ./src/summary/Dockerfile
target: production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-agents:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: lasuite/meet-agents
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
continue-on-error: true
with:
docker-build-args: '-f src/agents/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
docker-context: './src/agents'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ./src/agents
file: ./src/agents/Dockerfile
target: production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push:
name: ${{ matrix.service }}
strategy:
fail-fast: false
matrix:
include:
- service: backend
image_name: lasuite/meet-backend
context: .
file: ./Dockerfile
target: backend-production
- service: frontend
image_name: lasuite/meet-frontend
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
- service: frontend-dinum
image_name: lasuite/meet-frontend-dinum
context: .
file: ./docker/dinum-frontend/Dockerfile
target: frontend-production
- service: summary
image_name: lasuite/meet-summary
context: ./src/summary
file: ./src/summary/Dockerfile
target: production
- service: agents
image_name: lasuite/meet-agents
context: ./src/agents
file: ./src/agents/Dockerfile
target: production
uses: suitenumerique/ci/.github/workflows/_docker-publish.yml@main
with:
image_name: ${{ matrix.image_name }}
context: ${{ matrix.context }}
file: ${{ matrix.file }}
target: ${{ matrix.target }}
docker_user: "1001:127"
is_multi_platform: ${{ startsWith(github.ref, 'refs/tags/v') }}
should_push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
trivy_scan: true
secrets:
DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }}
DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
notify-argocd:
permissions:
contents: read
needs:
- build-and-push-frontend-generic
- build-and-push-frontend-dinum
- build-and-push-backend
- build-and-push-summary
- build-and-push-agents
- build-and-push
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- uses: numerique-gouv/action-argocd-webhook-notification@cac2ee67896eb13e84e804f60c4271370424eaa8 # main
- uses: suitenumerique/ci/actions/argocd-webhook-notification@main
id: notify
with:
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
+7 -23
View File
@@ -1,33 +1,17 @@
name: Release Chart
run-name: Release Chart
name: Release Helm chart
on:
push:
branches:
- main
paths:
- src/helm/meet/**
permissions:
contents: read
jobs:
release:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
- name: Cleanup
run: rm -rf ./src/helm/extra
- name: Install Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Publish Helm charts
uses: numerique-gouv/helm-gh-pages@2cf477ae49d7c70037ceb1685803f4f7bad9b981 # add-overwrite-option
with:
charts_dir: ./src/helm
linting: on
token: ${{ secrets.GITHUB_TOKEN }}
uses: suitenumerique/ci/.github/workflows/_release-helm-chart.yml@main
+21
View File
@@ -0,0 +1,21 @@
name: Security analysis
on:
push:
branches:
- main
pull_request:
branches:
- "**"
permissions: {}
jobs:
zizmor:
permissions:
contents: read
actions: read
security-events: write
uses: suitenumerique/ci/.github/workflows/_zizmor.yml@main
with:
config: .github/zizmor.yml
+5
View File
@@ -0,0 +1,5 @@
rules:
unpinned-uses:
config:
policies:
"suitenumerique/*": ref-pin
+12 -4
View File
@@ -8,12 +8,20 @@ and this project adheres to
## [Unreleased]
## [1.32.1] - 2026-09-25
### Fixed
- 🔒️(backend) fix CVE-2026-73228 and CVE-2026-73229 in drf
- 🔒️(agent) fix CRITICAL CVE-2026-63072 / CVE-2026-63073 in libssl3t64
## [1.32.0] - 2026-09-25
### Added
- ✨(backend) make the LiveKit default video codec configurable
- 🔧(dev) add support for Bureautix workstations
- ✨(frontend) add screen share zoom controls #1498
- ✨(frontend) open screen share in a separate window #1734
### Changed
@@ -29,13 +37,13 @@ and this project adheres to
- ⬆️(frontend) upgrade posthog-js from 1.414.0 to 1.418.10
- ⬆️(addons) upgrade i18next from 26.3.6 to 26.4.0
- ⬆️(frontend) upgrade humanize-duration from 3.33.2 to 3.34.1
- ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.1
- ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.2
- 🔖(helm) release chart 0.0.28
- ♻️(backend) decouple recording event handling from LiveKit egress statuses
### Fixed
- 🐛(helm) probe liveness on **lbheartbeat** and readiness on **heartbeat**
- 🐛(helm) probe liveness on __lbheartbeat__ and readiness on __heartbeat__
- 🐛(helm) render periodSeconds and failureThreshold on probes
- 🐛(backend) report the app release to Sentry instead of "NA"
- 🐛(frontend) play the waiting room notification sound on every arrival
@@ -366,7 +374,7 @@ and this project adheres to
### Fixed
- ♿️(frontend) improve accessibilty of the Effects panel #1401
- ♿️(frontend) improve accessibility of the Effects panel #1401
## [1.20.0] - 2026-06-12
+8 -8
View File
@@ -55,12 +55,12 @@ function _docker_compose() {
function _dc_run() {
_set_user
user_args="--user=$USER_ID"
if [ -z $USER_ID ]; then
user_args=""
user_args=()
if [ -n "$USER_ID" ]; then
user_args=("--user=$USER_ID")
fi
_docker_compose run --rm $user_args "$@"
_docker_compose run --rm "${user_args[@]}" "$@"
}
# _dc_exec: wrap docker compose exec command
@@ -74,12 +74,12 @@ function _dc_exec() {
echo "🐳(compose) exec command: '\$@'"
user_args="--user=$USER_ID"
if [ -z $USER_ID ]; then
user_args=""
user_args=()
if [ -n "$USER_ID" ]; then
user_args=("--user=$USER_ID")
fi
_docker_compose exec $user_args "$@"
_docker_compose exec "${user_args[@]}" "$@"
}
# _django_manage: wrap django's manage.py command with docker compose
+1 -1
View File
@@ -40,7 +40,7 @@ if [ -n "$CUSTOM_LOGO_URL" ]; then
[[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1
mv -f "$TMP_FILE" "$LOGO_FILE"
echo "[custom-logo] INFO: Custom logo downloaded successfuly"
echo "[custom-logo] INFO: Custom logo downloaded successfully"
fi
mv src/backend/* ./
+1 -1
View File
@@ -7,7 +7,7 @@ gunicorn -b 0.0.0.0:8000 meet.wsgi:application --log-file - &
bin/run &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
trap 'pkill -TERM -P $$' SIGTERM
# wait for a single child to finish,
wait -n
+4 -5
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -o errexit
CURRENT_DIR=$(pwd)
NAMESPACE=${1:-meet}
SECRET_NAME=${2:-bitwarden-cli-meet}
TEMP_SECRET_FILE=$(mktemp)
@@ -30,10 +29,10 @@ check_secret_exists() {
# Collect user input securely
get_user_input() {
echo "Please provide the following information:"
read -p "Enter your Vaultwarden email login: " LOGIN
read -s -p "Enter your Vaultwarden password: " PASSWORD
read -r -p "Enter your Vaultwarden email login: " LOGIN
read -r -s -p "Enter your Vaultwarden password: " PASSWORD
echo
read -p "Enter your Vaultwarden server url: " URL
read -r -p "Enter your Vaultwarden server url: " URL
}
# Create and apply the secret
@@ -77,7 +76,7 @@ main() {
exit 0
fi
echo -e ${TEMP_SECRET_FILE}
echo -e "${TEMP_SECRET_FILE}"
get_user_input
echo -e "\nCreating Vaultwarden secret…"
+2 -2
View File
@@ -3,7 +3,7 @@
mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/"
PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit"
cat <<'EOF' >$PRE_COMMIT_FILE
cat <<'EOF' >"$PRE_COMMIT_FILE"
#!/bin/bash
# directories containing potential secrets
@@ -27,4 +27,4 @@ for d in $DIRS; do
done
EOF
chmod +x $PRE_COMMIT_FILE
chmod +x "$PRE_COMMIT_FILE"
+1 -1
View File
@@ -68,7 +68,7 @@ fi
# Ask user for release version number
echo ""
read -p "Enter release version number (e.g., 1.2.3): " VERSION
read -r -p "Enter release version number (e.g., 1.2.3): " VERSION
# Validate version format (basic semver check)
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
git submodule update --init --recursive
# shellcheck disable=SC2016
git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
+1 -1
View File
@@ -8,6 +8,6 @@ environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
helmfile -e "$env" -f src/helm/helmfile.yaml lint || exit 1
echo -e "\n"
done
+1 -1
View File
@@ -1,7 +1,7 @@
# Bureautix proxy overrides
#
# Builds submitted through the Docker API of the Podman service get none of
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitely
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitly
# otherwise all connections fail during the build.
x-proxy-vars: &proxy-vars
+1 -1
View File
@@ -317,7 +317,7 @@ These are the environmental options available on meet backend.
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used for Django security | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow insecure 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 |
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": ["github>numerique-gouv/renovate-configuration"],
"extends": ["github>suitenumerique/ci//renovate/default"],
"dependencyDashboard": true,
"labels": ["dependencies", "noChangeLog"],
"packageRules": [
+4 -4
View File
@@ -10,7 +10,7 @@
"license": "MIT",
"dependencies": {
"core-js": "3.50.0",
"i18next": "26.4.1",
"i18next": "26.4.2",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
@@ -9367,9 +9367,9 @@
}
},
"node_modules/i18next": {
"version": "26.4.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.1.tgz",
"integrity": "sha512-9YbX5E6gd1H+yaOSX3izCsPj5iWXyH7X4oC+iuHHJJw8AeHglzOK5SJf+1CHxaYKYigcea+8jvzSxqYx46YvyA==",
"version": "26.4.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz",
"integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==",
"funding": [
{
"type": "individual",
+1 -1
View File
@@ -27,7 +27,7 @@
},
"dependencies": {
"core-js": "3.50.0",
"i18next": "26.4.1",
"i18next": "26.4.2",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
+1 -1
View File
@@ -21,7 +21,7 @@ const { initI18n, translateUI } = require("../common/i18n");
document.querySelector("#close-msg").style.display = "block";
})
.catch((e) => {
console.error(`Error occured: ${e}`);
console.error(`Error occurred: ${e}`);
})
.finally(() => {
// NOTE: doesn't work with the desktop client — the browser considers
+1
View File
@@ -5,6 +5,7 @@ RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sou
&& apt-get update && apt-get install -y --no-install-recommends \
libglib2.0-0 \
libgobject-2.0-0 \
libssl3t64 \
&& rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.31.0"
version = "1.32.1"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.6.7",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]]
name = "agents"
version = "1.31.0"
version = "1.32.1"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
@@ -24,7 +24,7 @@ class BaseEgressService:
def _get_filepath(self, filename: str, extension: str) -> str:
"""Construct the file path for a given filename and extension.
Unsecure method, doesn't handle paths robustly and securely.
Insecure method, doesn't handle paths robustly and securely.
"""
return f"{self._config.output_folder}/{filename}.{extension}"
@@ -27,7 +27,7 @@ def test_api_files_list_anonymous_not_allowed():
def test_api_files_list_authentificated_user_allowed():
"""
Authentificated users should be allowed to list files
Authenticated users should be allowed to list files
"""
user = factories.UserFactory()
client = APIClient()
@@ -458,7 +458,7 @@ def test_mute_participant_livekit_token_presence_check_twirp_error_forbidden(
room = RoomFactory()
mock_livekit_client.room.get_participant.side_effect = TwirpError(
msg="an error occured", code="not_found", status=500
msg="an error occurred", code="not_found", status=500
)
user = AnonymousUser()
@@ -1,5 +1,5 @@
"""
Test SIP mamagement service.
Test SIP management service.
"""
# pylint: disable=W0212
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.31.0"
version = "1.32.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -41,7 +41,7 @@ dependencies = [
"django-timezone-field>=5.1",
"django-pydantic-field==0.5.4",
"django==5.2.16",
"djangorestframework==3.17.1",
"djangorestframework==3.17.2",
"drf_spectacular==0.30.0",
"dockerflow==2026.3.4",
"easy_thumbnails==2.10.1",
+5 -5
View File
@@ -754,14 +754,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.17.1"
version = "3.17.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
{ url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" },
]
[[package]]
@@ -1187,7 +1187,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.31.0"
version = "1.32.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -1273,7 +1273,7 @@ requires-dist = [
{ name = "django-redis", specifier = "==7.0.0" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-timezone-field", specifier = ">=5.1" },
{ name = "djangorestframework", specifier = "==3.17.1" },
{ name = "djangorestframework", specifier = "==3.17.2" },
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "drf-spectacular", specifier = "==0.30.0" },
{ name = "easy-thumbnails", specifier = "==2.10.1" },
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.31.0",
"version": "1.32.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.31.0",
"version": "1.32.1",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
"@fontsource-variable/lexend": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.31.0",
"version": "1.32.1",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
+1 -1
View File
@@ -121,7 +121,7 @@ const config: Config = {
},
tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff.
* This way we'll only add the things we need step by step and prevent using lots of differents things.
* This way we'll only add the things we need step by step and prevent using lots of different things.
*/
...pandaPreset.theme.tokens,
colors: defineTokens.colors({
@@ -12,10 +12,6 @@ import {
import { Track } from 'livekit-client'
import { useSnapshot } from 'valtio'
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
import {
closeScreenSharePopout,
screenSharePopoutStore,
} from '@/stores/screenSharePopout'
import { useEffect, useRef } from 'react'
export const StageLayout = () => {
@@ -35,26 +31,8 @@ export const StageLayout = () => {
.filter((track) => track.publication.source === Track.Source.ScreenShare)
const { pinnedTrackRef } = useSnapshot(layoutStore)
const { entry: popoutEntry } = useSnapshot(screenSharePopoutStore)
const detachedSid = popoutEntry?.trackSid
// The popped-out share stays mounted below, but out of the grid and the
// carousel. It comes back with the other tracks when its window closes.
const visibleTracks = detachedSid
? tracks.filter(
(track) =>
!isTrackReference(track) || track.publication.trackSid !== detachedSid
)
: tracks
const detachedTrack = detachedSid
? tracks.find(
(track) =>
isTrackReference(track) && track.publication.trackSid === detachedSid
)
: undefined
const carouselTracks = visibleTracks.filter(
const carouselTracks = tracks.filter(
(track) => !isEqualTrackRef(track, pinnedTrackRef)
)
@@ -107,36 +85,11 @@ export const StageLayout = () => {
])
/* eslint-enable react-hooks/exhaustive-deps */
const screenShareKey = screenShareTracks
.map((track) => track.publication.trackSid)
.join()
// The popped-out tile is kept mounted below, so nothing else notices when
// the share stops. Close the window here instead.
useEffect(() => {
const entry = screenSharePopoutStore.entry
if (!entry) return
const alive = screenShareTracks.some(
(track) => track.publication.trackSid === entry.trackSid
)
if (!alive) closeScreenSharePopout({ restorePin: false })
// screenShareKey is the sid list; the array itself is new every render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenShareKey])
return (
<>
{/* Out of the layout, but still mounted: this subtree is what renders
into the separate window. hidden also takes it off the a11y tree
and out of the tab order. */}
{detachedTrack && (
<div hidden>
<ParticipantTile trackRef={detachedTrack} />
</div>
)}
{!pinnedTrackRef ? (
<div className="lk-grid-layout-wrapper" style={{ height: 'auto' }}>
<GridLayout tracks={visibleTracks} style={{ padding: 0 }}>
<GridLayout tracks={tracks} style={{ padding: 0 }}>
<ParticipantTile />
</GridLayout>
</div>
@@ -153,12 +153,7 @@ export const ParticipantTile: (
// there without the dead fullscreen button.
trackMedia =
isRemoteScreenShare && !disableTileControls ? (
<ScreenShareZoomableVideo
tileRef={tileRef}
participantName={participantName}
trackSid={trackReference.publication.trackSid}
windowName={`meet-screen-share-${trackReference.publication.trackSid || trackReference.participant.identity}`}
>
<ScreenShareZoomableVideo tileRef={tileRef}>
{videoTrack}
</ScreenShareZoomableVideo>
) : (
@@ -158,7 +158,7 @@ export const Conference = ({
*
* Issue: On Firefox behind proxy configurations, WebSocket signaling fails to establish.
* Symptom: Client receives HTTP 200 instead of expected 101 (Switching Protocols).
* Root Cause: Certificate/security issue where the initial request is considered unsecure.
* Root Cause: Certificate/security issue where the initial request is considered insecure.
*
* Solution: Pre-establish a WebSocket connection to the signaling server, which fails.
* This "primes" the connection, allowing subsequent WebSocket establishments to work correctly.
@@ -4,8 +4,6 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
const getOwnerDocument = (el: Element | null) => el?.ownerDocument ?? document
// Keeps the fullscreen state here rather than on the toolbar, so entering or
// leaving fullscreen does not re-render the zoom controls.
export const ScreenShareFullscreenButton = memo(
@@ -18,22 +16,15 @@ export const ScreenShareFullscreenButton = memo(
const announce = useScreenReaderAnnounce()
const [isFullscreen, setIsFullscreen] = useState(false)
const [isFullscreenAvailable, setIsFullscreenAvailable] = useState(
() => document.fullscreenEnabled
)
// Tracks whether this tile's container triggered fullscreen (vs another share's).
const wasThisTileFullscreen = useRef(false)
// Covers Esc and browser UI exits, not just this button.
// Listens on the element's own document, so it still works in the popup.
// Only this tile's instance announces to avoid duplicates with multiple shares.
useEffect(() => {
const doc = getOwnerDocument(containerRef.current)
setIsFullscreenAvailable(!!doc.fullscreenEnabled)
const onChange = () => {
const isThisTileFullscreen =
doc.fullscreenElement === containerRef.current
document.fullscreenElement === containerRef.current
setIsFullscreen(isThisTileFullscreen)
if (isThisTileFullscreen) {
@@ -44,17 +35,16 @@ export const ScreenShareFullscreenButton = memo(
announce(t('fullScreenExited'), 'assertive')
}
}
doc.addEventListener('fullscreenchange', onChange)
return () => doc.removeEventListener('fullscreenchange', onChange)
document.addEventListener('fullscreenchange', onChange)
return () => document.removeEventListener('fullscreenchange', onChange)
}, [announce, t, containerRef])
const toggleFullScreen = useCallback(async () => {
const doc = getOwnerDocument(containerRef.current)
try {
if (doc.fullscreenElement === containerRef.current) {
await doc.exitFullscreen()
if (document.fullscreenElement === containerRef.current) {
await document.exitFullscreen()
} else {
// Tile / pop-out chrome so zoom controls stay visible in fullscreen.
// Tile container so zoom controls stay visible in fullscreen.
await containerRef.current?.requestFullscreen()
}
} catch (error) {
@@ -62,7 +52,7 @@ export const ScreenShareFullscreenButton = memo(
}
}, [containerRef])
if (!isFullscreenAvailable) return null
if (!document.fullscreenEnabled) return null
return (
<Button
@@ -1,30 +0,0 @@
import { createPortal } from 'react-dom'
import { UNSAFE_PortalProvider } from '@react-aria/overlays'
import { ScreenReaderAnnouncer } from '@/primitives'
import { CrossDocumentOverlaysContext } from '@/primitives/CrossDocumentOverlaysContext'
/**
* Render in the popup. Tooltips have to live there too, or they show up
* in the meeting window instead.
*/
export const ScreenSharePopoutPortal = ({
container,
children,
}: {
container: HTMLElement
children: React.ReactNode
}) => {
if (!container.isConnected) return null
return createPortal(
<UNSAFE_PortalProvider getContainer={() => container}>
<CrossDocumentOverlaysContext.Provider value={true}>
{/* The meeting live region sits in the other document, which a screen
reader stops reading once the focus is here. */}
<ScreenReaderAnnouncer />
{children}
</CrossDocumentOverlaysContext.Provider>
</UNSAFE_PortalProvider>,
container
)
}
@@ -2,8 +2,6 @@ import { css } from '@/styled-system/css'
import { Button } from '@/primitives'
import {
RiFullscreenExitLine,
RiArrowGoBackLine,
RiShareBoxLine,
RiZoomInLine,
RiZoomOutLine,
} from '@remixicon/react'
@@ -12,7 +10,6 @@ import { Toolbar } from 'react-aria-components'
import { useEffect, useRef } from 'react'
import { isMacintosh } from '@/utils/livekit'
import { srOnly } from '@/styles/a11y'
import { useIsMobile } from '@/utils/useIsMobile'
import { ScreenShareFullscreenButton } from './ScreenShareFullscreenButton'
interface ScreenShareZoomControlsProps {
@@ -21,12 +18,9 @@ interface ScreenShareZoomControlsProps {
zoomPercentage: number
canZoomIn: boolean
canZoomOut: boolean
isPoppedOut: boolean
popoutButtonRef: React.Ref<HTMLButtonElement>
onZoomIn: () => void
onZoomOut: () => void
onResetZoom: () => void
onTogglePopout: () => void
}
export const ScreenShareZoomControls = ({
@@ -35,15 +29,11 @@ export const ScreenShareZoomControls = ({
zoomPercentage,
canZoomIn,
canZoomOut,
isPoppedOut,
popoutButtonRef,
onZoomIn,
onZoomOut,
onResetZoom,
onTogglePopout,
}: ScreenShareZoomControlsProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'screenShareZoom' })
const isMobile = useIsMobile()
const zoomInButtonRef = useRef<HTMLButtonElement>(null)
const hadFocusInCollapsibleRef = useRef(false)
@@ -168,28 +158,6 @@ export const ScreenShareZoomControls = ({
>
<RiZoomInLine size={20} />
</Button>
{/* Desktop only — popups on mobile are usually blocked. */}
{!isMobile && (
<Button
ref={popoutButtonRef}
size="sm"
variant="primaryTextDark"
square
tooltip={
isPoppedOut ? t('closeSeparateWindow') : t('openInSeparateWindow')
}
aria-label={
isPoppedOut ? t('closeSeparateWindow') : t('openInSeparateWindow')
}
onPress={onTogglePopout}
>
{isPoppedOut ? (
<RiArrowGoBackLine size={20} />
) : (
<RiShareBoxLine size={20} />
)}
</Button>
)}
<ScreenShareFullscreenButton containerRef={containerRef} />
</Toolbar>
</div>
@@ -1,83 +1,24 @@
import { css } from '@/styled-system/css'
import {
cloneElement,
isValidElement,
useCallback,
useEffect,
useLayoutEffect,
useRef,
type ReactNode,
} from 'react'
import { useEffect, useRef, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useScreenShareZoom } from '../hooks/useScreenShareZoom'
import { useScreenSharePopout } from '../hooks/useScreenSharePopout'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { ScreenShareZoomControls } from './ScreenShareZoomControls'
import { ScreenSharePopoutPortal } from './ScreenSharePopoutPortal'
import {
saveScreenShareZoom,
takePopoutButtonFocus,
takeScreenShareZoom,
} from '@/stores/screenSharePopout'
interface ScreenShareZoomableVideoProps {
tileRef: React.RefObject<HTMLDivElement | null>
participantName: string
trackSid: string
windowName: string
children: ReactNode
}
const popoutChromeClassName = css({
width: '100%',
height: '100%',
position: 'relative',
backgroundColor: 'primaryDark.50',
outline: 'none',
_focusVisible: {
outline: '2px solid',
outlineColor: 'primary.500',
outlineOffset: '-2px',
},
'& .lk-participant-media-video': {
width: '100%',
height: '100%',
objectFit: 'contain',
},
})
// The video comes in as children so that a zoom change, which only re-renders
// this wrapper, leaves the video subtree untouched.
export const ScreenShareZoomableVideo = ({
tileRef,
participantName,
trackSid,
windowName,
children,
}: ScreenShareZoomableVideoProps) => {
const zoom = useScreenShareZoom()
const { t } = useTranslation('rooms', { keyPrefix: 'screenShareZoom' })
const announce = useScreenReaderAnnounce()
const popoutChromeRef = useRef<HTMLDivElement>(null)
const popoutButtonRef = useRef<HTMLButtonElement>(null)
const wasPoppedOut = useRef(false)
const getVideoElement = useCallback(
() => zoom.transformElRef.current?.querySelector('video') ?? null,
[zoom.transformElRef]
)
const popout = useScreenSharePopout({
trackSid,
windowName,
title: t('separateWindowTitle', { name: participantName }),
getVideoElement,
})
// Moving the video in or out of the window remounts this tile, because the
// stage pin changes. Stash the zoom so the new instance picks it up.
const { capture, resync } = zoom
useLayoutEffect(() => {
return () => saveScreenShareZoom(trackSid, capture())
}, [trackSid, capture])
// SR announcement: announce zoom level on change, with a one-time pan hint
// on the first zoom above 100 % per session.
@@ -98,45 +39,23 @@ export const ScreenShareZoomableVideo = ({
if (!zoom.isZoomed) hasAnnouncedPanHint.current = false
}, [zoom.zoomPercentage, zoom.isZoomed, announce, t])
// Keys on the tile, or on the popup once the video is over there.
// Attach keyboard listener on the tile container (has tabIndex=0).
useEffect(() => {
const el = popout.isOpen ? popoutChromeRef.current : tileRef.current
const el = tileRef.current
if (!el) return
el.addEventListener('keydown', zoom.handleKeyDown)
return () => el.removeEventListener('keydown', zoom.handleKeyDown)
}, [popout.isOpen, tileRef, zoom.handleKeyDown])
}, [tileRef, zoom.handleKeyDown])
// Native wheel listener with { passive: false } so preventDefault works.
// Re-attach when the video moves to the other window.
useEffect(() => {
const el = zoom.surfaceElRef.current
if (!el) return
el.addEventListener('wheel', zoom.handleWheel, { passive: false })
return () => el.removeEventListener('wheel', zoom.handleWheel)
}, [zoom.handleWheel, zoom.surfaceElRef, popout.isOpen])
}, [zoom.handleWheel, zoom.surfaceElRef])
// Open: focus the popup. Close: focus the button again (the toolbar remounts).
useLayoutEffect(() => {
resync(takeScreenShareZoom(trackSid) ?? undefined)
if (popout.isOpen) {
wasPoppedOut.current = true
popoutChromeRef.current?.focus()
return
}
if (!wasPoppedOut.current && !takePopoutButtonFocus(trackSid)) return
wasPoppedOut.current = false
popoutButtonRef.current?.focus()
}, [popout.isOpen, resync, trackSid])
// LiveKit unsubscribes tiles it believes are off-screen. Its observer
// cannot measure an element living in another window and reads it as
// hidden, which would drop the track a few seconds after opening.
const video =
popout.isOpen && isValidElement<{ manageSubscription?: boolean }>(children)
? cloneElement(children, { manageSubscription: false })
: children
const media = (
return (
<>
<div
ref={zoom.surfaceElRef}
@@ -161,44 +80,19 @@ export const ScreenShareZoomableVideo = ({
transformOrigin: 'center center',
}}
>
{video}
{children}
</div>
</div>
<ScreenShareZoomControls
containerRef={popout.isOpen ? popoutChromeRef : tileRef}
containerRef={tileRef}
isZoomed={zoom.isZoomed}
zoomPercentage={zoom.zoomPercentage}
canZoomIn={zoom.canZoomIn}
canZoomOut={zoom.canZoomOut}
isPoppedOut={popout.isOpen}
popoutButtonRef={popoutButtonRef}
onZoomIn={zoom.zoomIn}
onZoomOut={zoom.zoomOut}
onResetZoom={zoom.resetZoom}
onTogglePopout={popout.toggle}
/>
</>
)
// Video in the popup. The meeting layout does not keep a tile for it.
if (popout.isOpen && popout.container) {
return (
<ScreenSharePopoutPortal container={popout.container}>
<div
ref={popoutChromeRef}
role="group"
// Focusable on purpose: it carries the zoom key handler, and the
// popup body is an ancestor, so keys would never bubble to it.
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
aria-label={t('separateWindowLabel', { name: participantName })}
className={popoutChromeClassName}
>
{media}
</div>
</ScreenSharePopoutPortal>
)
}
return media
}
@@ -1,112 +0,0 @@
import { useCallback } from 'react'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'react-i18next'
import { reportError } from '@/features/analytics/telemetry'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import {
closeScreenSharePopout,
openScreenSharePopout,
screenSharePopoutStore,
} from '@/stores/screenSharePopout'
import {
getAuxiliaryWindowFeatures,
getAuxiliaryWindowSize,
initializeAuxiliaryWindow,
} from '@/utils/auxiliaryWindow'
type UseScreenSharePopoutOptions = {
trackSid: string
windowName: string
title: string
getVideoElement?: () => HTMLVideoElement | null
}
/**
* Opens the screen share in another window. Closing it does not stop the
* share: the video just comes back into the meeting.
*
* The window lives in a store, not in this hook. Opening it drops the stage
* pin, so the tile moves from the focus layout into the grid and this
* component remounts. The store is what keeps the window open across that.
*
* A real popup, not the meeting PiP, that one is already taken, and a
* popup can be as large as another screen.
*/
export const useScreenSharePopout = ({
trackSid,
windowName,
title,
getVideoElement,
}: UseScreenSharePopoutOptions) => {
const { t } = useTranslation('rooms', { keyPrefix: 'screenShareZoom' })
const announce = useScreenReaderAnnounce()
const { entry } = useSnapshot(screenSharePopoutStore)
const isOpen = entry?.trackSid === trackSid
// Brings the video back into the meeting, from the toolbar button as well as
// from the window's own close button. Safe to call after this hook's
// component has unmounted: the listener sits on the popup, not on the tile.
const release = useCallback(() => {
if (screenSharePopoutStore.entry?.trackSid !== trackSid) return
closeScreenSharePopout({ restorePin: true })
announce(t('separateWindowClosed'), 'assertive')
}, [announce, t, trackSid])
const open = useCallback(() => {
if (screenSharePopoutStore.entry) return
const { width, height } = getAuxiliaryWindowSize(getVideoElement?.())
// Open right away: waiting first (fullscreen, etc.) lets the browser
// block the popup.
const next = window.open(
'',
windowName,
getAuxiliaryWindowFeatures(width, height)
)
if (!next) {
announce(t('separateWindowBlocked'), 'assertive')
return
}
try {
const container = initializeAuxiliaryWindow(next, { title })
openScreenSharePopout({
trackSid,
popup: next,
container,
// The window X does not go through close() — still bring the video back.
onPopupClosed: release,
})
next.focus()
announce(t('separateWindowOpened'), 'assertive')
// Drop meeting fullscreen: the stage this share was filling goes away.
if (document.fullscreenElement) {
void document.exitFullscreen()
}
} catch (error) {
reportError('generic_failure', error, {
context: 'screen_share_popout_init',
})
next.close()
}
}, [announce, getVideoElement, release, t, title, trackSid, windowName])
const toggle = useCallback(() => {
if (screenSharePopoutStore.entry?.trackSid === trackSid) release()
else open()
}, [open, release, trackSid])
return {
isOpen,
// The snapshot deep-freezes the element. The portal needs the real node,
// which `ref()` kept out of the proxy.
container: isOpen
? (screenSharePopoutStore.entry?.container ?? null)
: null,
open,
close: release,
toggle,
}
}
@@ -19,8 +19,6 @@ import {
getZoomTransform,
} from '../utils/screenShareZoom'
export type ZoomState = { zoom: number; pan: PanOffset }
/**
* Manages zoom and pan state for a remote screen share.
*
@@ -81,28 +79,6 @@ export const useScreenShareZoom = () => {
)
}, [])
// After the video moves to the other window, write the current zoom back
// on the new nodes (otherwise it looks like 100 % until the next scroll).
// Pass a state to adopt one captured before a remount; the pan is clamped
// against the new container either way.
const resync = useCallback(
(state?: ZoomState) => {
if (state) {
zoomRef.current = state.zoom
panRef.current = state.pan
}
panRef.current = clampPan(
panRef.current,
zoomRef.current,
readPictureRatio()
)
applyTransform()
applyCursor()
syncToolbar()
},
[applyCursor, applyTransform, syncToolbar, readPictureRatio]
)
const setZoom = useCallback(
(next: number) => {
zoomRef.current = next
@@ -277,13 +253,6 @@ export const useScreenShareZoom = () => {
[panBy, zoomIn, zoomOut, resetZoom]
)
// The tile remounts when the layout switches. Callers stash this across
// that remount so the popup keeps the zoom the user already had.
const capture = useCallback(
(): ZoomState => ({ zoom: zoomRef.current, pan: { ...panRef.current } }),
[]
)
return {
zoomPercentage: Math.round(zoomLevel * 100),
isZoomed: zoomLevel > MIN_ZOOM,
@@ -295,8 +264,6 @@ export const useScreenShareZoom = () => {
zoomIn,
zoomOut,
resetZoom,
resync,
capture,
handleWheel,
handleKeyDown,
}
@@ -1,7 +1,7 @@
import { isWeb } from '@livekit/components-core'
import { MediaDeviceFailure, Track } from 'livekit-client'
import { getMediaDeviceFailure } from '../utils/mediaPermissions'
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import {
ConnectionStateToast,
RoomAudioRenderer,
@@ -32,7 +32,6 @@ import { ChatProvider } from '@/features/chat/components/ChatProvider'
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
import { RoomSilentMicDetector } from '@/features/rooms/components/SilentMicDetector'
import { LobbyProvider } from '@/features/rooms/components/LobbyProvider'
import { closeScreenSharePopout } from '@/stores/screenSharePopout'
/**
* @public
@@ -79,12 +78,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const { isOpen: isPictureInPictureOpen } = usePictureInPicture()
// Picture-in-picture replaces the stage, which is what renders a popped-out
// share. Bring it back rather than leave an empty window behind.
useEffect(() => {
if (isPictureInPictureOpen) closeScreenSharePopout({ restorePin: true })
}, [isPictureInPictureOpen])
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
const handleDeviceError = ({
+1 -8
View File
@@ -772,14 +772,7 @@
"currentZoomLevel": "Zoom {{level}} %",
"panHint": "Zoom {{level}} %. Mit der Maus ziehen oder den Fokus zurück auf die Bildschirmfreigabe setzen und mit den Pfeiltasten im Bild navigieren.",
"fullScreenEntered": "Vollbild aktiviert",
"fullScreenExited": "Vollbild deaktiviert",
"openInSeparateWindow": "In eigenem Fenster öffnen",
"closeSeparateWindow": "Bildschirmfreigabe zurück in die Besprechung holen",
"separateWindowOpened": "Bildschirmfreigabe in einem eigenen Fenster geöffnet",
"separateWindowClosed": "Bildschirmfreigabe zurück in die Besprechung geholt",
"separateWindowBlocked": "Der Browser hat das eigene Fenster blockiert. Erlauben Sie Pop-ups für diese Website und versuchen Sie es erneut.",
"separateWindowTitle": "Bildschirmfreigabe von {{name}}",
"separateWindowLabel": "Bildschirmfreigabe von {{name}} in einem eigenen Fenster"
"fullScreenExited": "Vollbild deaktiviert"
},
"shortcutsPanel": {
"title": "Tastenkürzel",
+1 -8
View File
@@ -772,14 +772,7 @@
"currentZoomLevel": "Zoom {{level}} %",
"panHint": "Zoom {{level}} %. Drag with mouse, or move focus back to the screen share and use arrow keys to navigate the picture.",
"fullScreenEntered": "Full screen enabled",
"fullScreenExited": "Full screen disabled",
"openInSeparateWindow": "Open in separate window",
"closeSeparateWindow": "Return screen share to meeting",
"separateWindowOpened": "Screen share opened in a separate window",
"separateWindowClosed": "Screen share returned to the meeting",
"separateWindowBlocked": "The browser blocked the separate window. Allow pop-ups for this site and try again.",
"separateWindowTitle": "{{name}}'s screen share",
"separateWindowLabel": "{{name}}'s screen share in a separate window"
"fullScreenExited": "Full screen disabled"
},
"shortcutsPanel": {
"title": "Keyboard shortcuts",
+1 -8
View File
@@ -771,14 +771,7 @@
"currentZoomLevel": "Zoom {{level}} %",
"panHint": "Zoom {{level}} %. Arrastra con el ratón, o vuelve a la pantalla compartida y usa las flechas para moverte por la imagen.",
"fullScreenEntered": "Pantalla completa activada",
"fullScreenExited": "Pantalla completa desactivada",
"openInSeparateWindow": "Abrir en una ventana separada",
"closeSeparateWindow": "Devolver la pantalla compartida a la reunión",
"separateWindowOpened": "Pantalla compartida abierta en una ventana separada",
"separateWindowClosed": "Pantalla compartida devuelta a la reunión",
"separateWindowBlocked": "El navegador bloqueó la ventana separada. Permite las ventanas emergentes para este sitio y vuelve a intentarlo.",
"separateWindowTitle": "Pantalla compartida de {{name}}",
"separateWindowLabel": "Pantalla compartida de {{name}} en una ventana separada"
"fullScreenExited": "Pantalla completa desactivada"
},
"shortcutsPanel": {
"title": "Atajos de teclado",
+1 -8
View File
@@ -772,14 +772,7 @@
"currentZoomLevel": "Zoom {{level}} %",
"panHint": "Zoom {{level}} %. Glissez avec la souris, ou revenez sur le partage d'écran et utilisez les touches fléchées pour naviguer dans l'image.",
"fullScreenEntered": "Plein écran activé",
"fullScreenExited": "Plein écran désactivé",
"openInSeparateWindow": "Ouvrir dans une fenêtre séparée",
"closeSeparateWindow": "Ramener le partage d'écran dans la réunion",
"separateWindowOpened": "Partage d'écran ouvert dans une fenêtre séparée",
"separateWindowClosed": "Partage d'écran ramené dans la réunion",
"separateWindowBlocked": "Le navigateur a bloqué la fenêtre séparée. Autorisez les pop-ups pour ce site, puis réessayez.",
"separateWindowTitle": "Partage d'écran de {{name}}",
"separateWindowLabel": "Partage d'écran de {{name}} dans une fenêtre séparée"
"fullScreenExited": "Plein écran désactivé"
},
"shortcutsPanel": {
"title": "Raccourcis clavier",
+1 -8
View File
@@ -772,14 +772,7 @@
"currentZoomLevel": "Zoom {{level}} %",
"panHint": "Zoom {{level}} %. Sleep met de muis, of zet de focus terug op de schermdeling en gebruik de pijltjestoetsen om door het beeld te navigeren.",
"fullScreenEntered": "Volledig scherm ingeschakeld",
"fullScreenExited": "Volledig scherm uitgeschakeld",
"openInSeparateWindow": "Openen in een apart venster",
"closeSeparateWindow": "Schermdeling terugzetten in de vergadering",
"separateWindowOpened": "Schermdeling geopend in een apart venster",
"separateWindowClosed": "Schermdeling teruggezet in de vergadering",
"separateWindowBlocked": "De browser heeft het aparte venster geblokkeerd. Sta pop-ups toe voor deze site en probeer het opnieuw.",
"separateWindowTitle": "Schermdeling van {{name}}",
"separateWindowLabel": "Schermdeling van {{name}} in een apart venster"
"fullScreenExited": "Volledig scherm uitgeschakeld"
},
"shortcutsPanel": {
"title": "Sneltoetsen",
@@ -1,123 +0,0 @@
import { flushSync } from 'react-dom'
import { proxy, ref } from 'valtio'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import type { ZoomState } from '@/features/rooms/livekit/hooks/useScreenShareZoom'
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
type Entry = {
trackSid: string
window: Window
container: HTMLElement
// Pin that was on screen when the window opened. Restored on close so the
// share (or whichever tile was focused) comes back. Absent when the meeting
// was already in grid view.
pinnedTrack?: TrackReferenceOrPlaceholder
detachListeners: () => void
}
export const screenSharePopoutStore = proxy<{ entry: Entry | null }>({
entry: null,
})
// Opening and closing both move the video between documents, which remounts
// the tile. These two slots carry what must survive that remount. They are
// armed by the transitions below, so an unrelated remount keeps its previous
// behaviour: the zoom resets and the focus stays where it was.
let armedSid: string | null = null
let carriedZoom: { trackSid: string; state: ZoomState } | null = null
let pendingButtonFocusSid: string | null = null
export const saveScreenShareZoom = (trackSid: string, state: ZoomState) => {
if (armedSid !== trackSid) return
// One save per transition, so a later unrelated remount starts from scratch.
armedSid = null
carriedZoom = { trackSid, state }
}
export const takeScreenShareZoom = (trackSid: string) => {
if (carriedZoom?.trackSid !== trackSid) return null
const { state } = carriedZoom
carriedZoom = null
return state
}
export const takePopoutButtonFocus = (trackSid: string) => {
if (pendingButtonFocusSid !== trackSid) return false
pendingButtonFocusSid = null
return true
}
export const openScreenSharePopout = ({
trackSid,
popup,
container,
onPopupClosed,
}: {
trackSid: string
popup: Window
container: HTMLElement
onPopupClosed: () => void
}) => {
const popupHidden = () => onPopupClosed()
// The meeting tab is going away: drop the window rather than run the
// bring-back flow, which would touch React while the page tears down.
const openerHidden = () => {
popup.removeEventListener('pagehide', popupHidden)
popup.close()
}
popup.addEventListener('pagehide', popupHidden, { once: true })
window.addEventListener('pagehide', openerHidden)
const pinnedTrack = layoutStore.pinnedTrackRef
armedSid = trackSid
screenSharePopoutStore.entry = {
trackSid,
window: ref(popup),
container: ref(container),
pinnedTrack: pinnedTrack ? ref(pinnedTrack) : undefined,
detachListeners: () => {
popup.removeEventListener('pagehide', popupHidden)
window.removeEventListener('pagehide', openerHidden)
},
}
// The share (or another pin) was filling the stage. Grid view leaves the
// rest of the meeting the whole window while the share is outside.
if (pinnedTrack) clearPinnedTrack()
}
// Clears the popout. The previous pin is restored only when the video is
// coming back: if the share itself ended, that pin points at a dead track.
export const closeScreenSharePopout = ({
restorePin,
}: {
restorePin: boolean
}) => {
const entry = screenSharePopoutStore.entry
if (!entry) return
const { window: popup, pinnedTrack, trackSid, detachListeners } = entry
detachListeners()
if (restorePin) {
// Both must be set before the render below: it remounts the tile, which
// reads them from its layout effect.
armedSid = trackSid
pendingButtonFocusSid = trackSid
// Put the cursor back in the meeting so the tile can focus its button.
window.focus()
} else {
armedSid = null
carriedZoom = null
pendingButtonFocusSid = null
}
// Unmount the portal while the other document is still alive, and put the
// pin back in the same render so the share doesn't flash through the grid.
flushSync(() => {
screenSharePopoutStore.entry = null
if (restorePin && pinnedTrack && !layoutStore.pinnedTrackRef) {
setPinnedTrack(pinnedTrack)
}
})
popup.close()
armedSid = null
}
-89
View File
@@ -1,89 +0,0 @@
// Helpers for a separate window. Not the meeting PiP: that API allows only
// one window, and it is already used. A blank popup has no CSS, so we copy
// styles from the meeting.
const AUXILIARY_ROOT_ID = 'root'
const copyDocumentChrome = (target: Window) => {
const { document: targetDoc } = target
document.head
.querySelectorAll('link[rel="stylesheet"], style')
.forEach((node) => {
targetDoc.head.appendChild(node.cloneNode(true))
})
targetDoc.documentElement.className = document.documentElement.className
targetDoc.documentElement.style.cssText =
document.documentElement.style.cssText
targetDoc.documentElement.setAttribute(
'lang',
document.documentElement.lang || 'en'
)
const theme = document.documentElement.dataset.lkTheme
if (theme) {
targetDoc.documentElement.dataset.lkTheme = theme
}
}
const ensureAuxiliaryRoot = (target: Window) => {
const existing = target.document.getElementById(AUXILIARY_ROOT_ID)
if (existing) return existing
const root = target.document.createElement('div')
root.id = AUXILIARY_ROOT_ID
root.style.width = '100%'
root.style.height = '100%'
target.document.body.appendChild(root)
return root
}
// Fill the window and reuse the meeting colors so it does not flash white.
const applyLayout = (target: Window) => {
const { document: targetDoc } = target
const sourceBody = getComputedStyle(document.body)
targetDoc.documentElement.style.height = '100%'
targetDoc.body.style.margin = '0'
targetDoc.body.style.height = '100%'
targetDoc.body.style.overflow = 'hidden'
targetDoc.body.style.backgroundColor = sourceBody.backgroundColor
targetDoc.body.style.color = sourceBody.color
}
// Returns the element to portal into.
export const initializeAuxiliaryWindow = (
target: Window,
{ title }: { title: string }
) => {
copyDocumentChrome(target)
target.document.title = title
applyLayout(target)
return ensureAuxiliaryRoot(target)
}
// Match the shared screen size, but keep the window on one display.
export const getAuxiliaryWindowSize = (
video?: Pick<HTMLVideoElement, 'videoWidth' | 'videoHeight'> | null
) => {
const maxWidth = Math.max(320, Math.round(window.screen.availWidth * 0.9))
const maxHeight = Math.max(240, Math.round(window.screen.availHeight * 0.9))
const videoWidth = video?.videoWidth ?? 0
const videoHeight = video?.videoHeight ?? 0
if (videoWidth > 0 && videoHeight > 0) {
const scale = Math.min(maxWidth / videoWidth, maxHeight / videoHeight, 1)
return {
width: Math.max(320, Math.round(videoWidth * scale)),
height: Math.max(240, Math.round(videoHeight * scale)),
}
}
return {
width: Math.min(1280, maxWidth),
height: Math.min(720, maxHeight),
}
}
export const getAuxiliaryWindowFeatures = (width: number, height: number) =>
`popup=yes,width=${width},height=${height},resizable=yes,scrollbars=no,status=no,location=no,toolbar=no,menubar=no`
+3 -4
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env bash
docker image ls | grep readme-generator-for-helm
if [ "$?" -ne "0" ]; then
if ! docker image ls | grep readme-generator-for-helm; then
git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm
cd /tmp/readme-generator-for-helm
cd /tmp/readme-generator-for-helm || exit 1
docker build -t readme-generator-for-helm:latest .
cd $(dirname -- "${BASH_SOURCE[0]}")
cd "$(dirname -- "${BASH_SOURCE[0]}")" || exit 1
fi
docker run --rm -it -v .:/source -w /source readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md
+1 -1
View File
@@ -51,7 +51,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
transform dictionnary of environment variables
transform dictionary of environment variables
Usage : {{ include "meet.env.transformDict" .Values.envVars }}
Example:
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.31.0",
"version": "1.32.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.31.0",
"version": "1.32.1",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.6.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.31.0",
"version": "1.32.1",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.31.0",
"version": "1.32.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.31.0",
"version": "1.32.1",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.31.0",
"version": "1.32.1",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.31.0"
version = "1.32.1"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]>=0.105.0",
+1 -1
View File
@@ -1516,7 +1516,7 @@ wheels = [
[[package]]
name = "summary"
version = "1.31.0"
version = "1.32.1"
source = { editable = "." }
dependencies = [
{ name = "celery" },