mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-29 22:19:08 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 609ea778f1 | |||
| a050134a48 | |||
| f7f793bc0c | |||
| f4aecdaf5a |
+34
-34
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install uv
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
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
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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)
|
||||
@@ -77,15 +77,15 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: "24"
|
||||
node-version: "22"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
@@ -119,13 +119,13 @@ jobs:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -145,13 +145,13 @@ jobs:
|
||||
working-directory: src/agents
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
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
|
||||
@@ -168,13 +168,13 @@ jobs:
|
||||
working-directory: src/summary
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
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
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:5
|
||||
ports:
|
||||
- 6379:6379
|
||||
# Set health checks to wait until redis has started
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -280,11 +280,11 @@ jobs:
|
||||
mc mb meet/meet-media-storage"
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
@@ -331,12 +331,12 @@ jobs:
|
||||
sudo apt-get install -y ffmpeg
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||
@@ -370,7 +370,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Download Crowdin files
|
||||
uses: crowdin/github-action@9af557de76d70c480f88065d336f445a362f402b # v3.1.0
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
@@ -30,24 +30,24 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
@@ -76,24 +76,24 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
@@ -123,24 +123,24 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
@@ -170,24 +170,24 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
@@ -219,24 +219,24 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: rm -rf ./src/helm/extra
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
|
||||
+3
-11
@@ -8,20 +8,12 @@ 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
|
||||
|
||||
@@ -37,13 +29,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.2
|
||||
- ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.1
|
||||
- 🔖(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
|
||||
|
||||
Generated
+4
-4
@@ -10,7 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.4.2",
|
||||
"i18next": "26.4.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
@@ -9367,9 +9367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.4.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz",
|
||||
"integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==",
|
||||
"version": "26.4.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.1.tgz",
|
||||
"integrity": "sha512-9YbX5E6gd1H+yaOSX3izCsPj5iWXyH7X4oC+iuHHJJw8AeHglzOK5SJf+1CHxaYKYigcea+8jvzSxqYx46YvyA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.4.2",
|
||||
"i18next": "26.4.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.6.7",
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
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.2",
|
||||
"djangorestframework==3.17.1",
|
||||
"drf_spectacular==0.30.0",
|
||||
"dockerflow==2026.3.4",
|
||||
"easy_thumbnails==2.10.1",
|
||||
|
||||
Generated
+5
-5
@@ -754,14 +754,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.17.2"
|
||||
version = "3.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1187,7 +1187,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
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.2" },
|
||||
{ name = "djangorestframework", specifier = "==3.17.1" },
|
||||
{ name = "dockerflow", specifier = "==2026.3.4" },
|
||||
{ name = "drf-spectacular", specifier = "==0.30.0" },
|
||||
{ name = "easy-thumbnails", specifier = "==2.10.1" },
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||
"@fontsource-variable/lexend": "5.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -12,6 +12,10 @@ 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 = () => {
|
||||
@@ -31,8 +35,26 @@ export const StageLayout = () => {
|
||||
.filter((track) => track.publication.source === Track.Source.ScreenShare)
|
||||
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
const { entry: popoutEntry } = useSnapshot(screenSharePopoutStore)
|
||||
const detachedSid = popoutEntry?.trackSid
|
||||
|
||||
const carouselTracks = tracks.filter(
|
||||
// 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(
|
||||
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
||||
)
|
||||
|
||||
@@ -85,11 +107,36 @@ 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={tracks} style={{ padding: 0 }}>
|
||||
<GridLayout tracks={visibleTracks} style={{ padding: 0 }}>
|
||||
<ParticipantTile />
|
||||
</GridLayout>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +153,12 @@ export const ParticipantTile: (
|
||||
// there without the dead fullscreen button.
|
||||
trackMedia =
|
||||
isRemoteScreenShare && !disableTileControls ? (
|
||||
<ScreenShareZoomableVideo tileRef={tileRef}>
|
||||
<ScreenShareZoomableVideo
|
||||
tileRef={tileRef}
|
||||
participantName={participantName}
|
||||
trackSid={trackReference.publication.trackSid}
|
||||
windowName={`meet-screen-share-${trackReference.publication.trackSid || trackReference.participant.identity}`}
|
||||
>
|
||||
{videoTrack}
|
||||
</ScreenShareZoomableVideo>
|
||||
) : (
|
||||
|
||||
@@ -4,6 +4,8 @@ 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(
|
||||
@@ -16,15 +18,22 @@ 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 =
|
||||
document.fullscreenElement === containerRef.current
|
||||
doc.fullscreenElement === containerRef.current
|
||||
setIsFullscreen(isThisTileFullscreen)
|
||||
|
||||
if (isThisTileFullscreen) {
|
||||
@@ -35,16 +44,17 @@ export const ScreenShareFullscreenButton = memo(
|
||||
announce(t('fullScreenExited'), 'assertive')
|
||||
}
|
||||
}
|
||||
document.addEventListener('fullscreenchange', onChange)
|
||||
return () => document.removeEventListener('fullscreenchange', onChange)
|
||||
doc.addEventListener('fullscreenchange', onChange)
|
||||
return () => doc.removeEventListener('fullscreenchange', onChange)
|
||||
}, [announce, t, containerRef])
|
||||
|
||||
const toggleFullScreen = useCallback(async () => {
|
||||
const doc = getOwnerDocument(containerRef.current)
|
||||
try {
|
||||
if (document.fullscreenElement === containerRef.current) {
|
||||
await document.exitFullscreen()
|
||||
if (doc.fullscreenElement === containerRef.current) {
|
||||
await doc.exitFullscreen()
|
||||
} else {
|
||||
// Tile container so zoom controls stay visible in fullscreen.
|
||||
// Tile / pop-out chrome so zoom controls stay visible in fullscreen.
|
||||
await containerRef.current?.requestFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -52,7 +62,7 @@ export const ScreenShareFullscreenButton = memo(
|
||||
}
|
||||
}, [containerRef])
|
||||
|
||||
if (!document.fullscreenEnabled) return null
|
||||
if (!isFullscreenAvailable) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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,6 +2,8 @@ import { css } from '@/styled-system/css'
|
||||
import { Button } from '@/primitives'
|
||||
import {
|
||||
RiFullscreenExitLine,
|
||||
RiArrowGoBackLine,
|
||||
RiShareBoxLine,
|
||||
RiZoomInLine,
|
||||
RiZoomOutLine,
|
||||
} from '@remixicon/react'
|
||||
@@ -10,6 +12,7 @@ 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 {
|
||||
@@ -18,9 +21,12 @@ 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 = ({
|
||||
@@ -29,11 +35,15 @@ 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)
|
||||
@@ -158,6 +168,28 @@ 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,24 +1,83 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useEffect, useRef, type ReactNode } from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
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
|
||||
}
|
||||
|
||||
// The video comes in as children so that a zoom change, which only re-renders
|
||||
// this wrapper, leaves the video subtree untouched.
|
||||
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',
|
||||
},
|
||||
})
|
||||
|
||||
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.
|
||||
@@ -39,23 +98,45 @@ export const ScreenShareZoomableVideo = ({
|
||||
if (!zoom.isZoomed) hasAnnouncedPanHint.current = false
|
||||
}, [zoom.zoomPercentage, zoom.isZoomed, announce, t])
|
||||
|
||||
// Attach keyboard listener on the tile container (has tabIndex=0).
|
||||
// Keys on the tile, or on the popup once the video is over there.
|
||||
useEffect(() => {
|
||||
const el = tileRef.current
|
||||
const el = popout.isOpen ? popoutChromeRef.current : tileRef.current
|
||||
if (!el) return
|
||||
el.addEventListener('keydown', zoom.handleKeyDown)
|
||||
return () => el.removeEventListener('keydown', zoom.handleKeyDown)
|
||||
}, [tileRef, zoom.handleKeyDown])
|
||||
}, [popout.isOpen, 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])
|
||||
}, [zoom.handleWheel, zoom.surfaceElRef, popout.isOpen])
|
||||
|
||||
return (
|
||||
// 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 = (
|
||||
<>
|
||||
<div
|
||||
ref={zoom.surfaceElRef}
|
||||
@@ -80,19 +161,44 @@ export const ScreenShareZoomableVideo = ({
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{video}
|
||||
</div>
|
||||
</div>
|
||||
<ScreenShareZoomControls
|
||||
containerRef={tileRef}
|
||||
containerRef={popout.isOpen ? popoutChromeRef : 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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,6 +19,8 @@ import {
|
||||
getZoomTransform,
|
||||
} from '../utils/screenShareZoom'
|
||||
|
||||
export type ZoomState = { zoom: number; pan: PanOffset }
|
||||
|
||||
/**
|
||||
* Manages zoom and pan state for a remote screen share.
|
||||
*
|
||||
@@ -79,6 +81,28 @@ 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
|
||||
@@ -253,6 +277,13 @@ 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,
|
||||
@@ -264,6 +295,8 @@ 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, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
ConnectionStateToast,
|
||||
RoomAudioRenderer,
|
||||
@@ -32,6 +32,7 @@ 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
|
||||
@@ -78,6 +79,12 @@ 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 = ({
|
||||
|
||||
@@ -772,7 +772,14 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Tastenkürzel",
|
||||
|
||||
@@ -772,7 +772,14 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Keyboard shortcuts",
|
||||
|
||||
@@ -771,7 +771,14 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Atajos de teclado",
|
||||
|
||||
@@ -772,7 +772,14 @@
|
||||
"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é"
|
||||
"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"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Raccourcis clavier",
|
||||
|
||||
@@ -772,7 +772,14 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Sneltoetsen",
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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`
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
|
||||
Generated
+1
-1
@@ -1516,7 +1516,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "summary"
|
||||
version = "1.32.1"
|
||||
version = "1.31.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "celery" },
|
||||
|
||||
Reference in New Issue
Block a user