mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-28 04:39:16 +00:00
Compare commits
126 Commits
v0.1.5
...
v-hackathon
| Author | SHA1 | Date | |
|---|---|---|---|
| 808fc7a00f | |||
| c8b4592d88 | |||
| db071f0dcc | |||
| b1b28fe330 | |||
| 98950f43af | |||
| b43f9922b6 | |||
| d49dc72849 | |||
| 82f0251101 | |||
| 2f675c2a3f | |||
| 7105c7cbae | |||
| 376ff8982c | |||
| a4820ae867 | |||
| 2a56cda55c | |||
| 817cf25b37 | |||
| fdaf567f5d | |||
| 0cf5ab1825 | |||
| c9c5d3b452 | |||
| f0b250739c | |||
| 41c9693d10 | |||
| 83cfcacc0e | |||
| e8618099ac | |||
| ac183c9eb9 | |||
| 3dcc93b630 | |||
| 3614b1e803 | |||
| 5e57647b34 | |||
| e271c87a20 | |||
| 40d1f01277 | |||
| 8477296471 | |||
| d200eeb6bd | |||
| 7a51b09664 | |||
| ec22abf82b | |||
| cde4b10794 | |||
| a47f1f92c4 | |||
| 0db36c788b | |||
| a84b76170d | |||
| 998382020d | |||
| 2a12715673 | |||
| 54d4330a97 | |||
| 5dcbe56e56 | |||
| 1a52221ef2 | |||
| 2f3e64b389 | |||
| 2dcaf814e1 | |||
| 583f5b8e70 | |||
| fe8fd36467 | |||
| 0370d9cad0 | |||
| 0da8aa846a | |||
| 70ed99b6c9 | |||
| 1875a394c6 | |||
| 211ba332dc | |||
| 7333c21632 | |||
| 78ebd1a8fd | |||
| 682b69fc11 | |||
| 7a73bf8fc2 | |||
| 1e934957f5 | |||
| 5a7584a3ad | |||
| fb9bf6b08e | |||
| 11b8541005 | |||
| 64607ae8d0 | |||
| c403bbc347 | |||
| 62855fe12c | |||
| f27d451eb6 | |||
| 68792d8019 | |||
| ebb8c14eeb | |||
| 15d43b8d5e | |||
| 1faae96ccd | |||
| 651cc0e5bd | |||
| 0dadd472ff | |||
| be35ee0258 | |||
| bece79f47b | |||
| a499331960 | |||
| d50d167d0a | |||
| e4c7bc0826 | |||
| b5244a5ec0 | |||
| 8cc2cc83c6 | |||
| e591d09b00 | |||
| eeb4dae12d | |||
| 271389d459 | |||
| fe6eefa1f0 | |||
| a276517278 | |||
| 9e9b9015f4 | |||
| 3f0378aada | |||
| 90c88a8bd3 | |||
| 1103902c12 | |||
| 812016d09c | |||
| cadc5c0034 | |||
| b083d837f8 | |||
| db8445f4ab | |||
| 0e5c2be445 | |||
| 0131a65509 | |||
| fb3727e73e | |||
| 3391165e4b | |||
| 0be94aa572 | |||
| b309f91095 | |||
| c50a749293 | |||
| cd9e7c8a1f | |||
| 9d0767ccfe | |||
| 021e52d9eb | |||
| 607a5fc99d | |||
| 756be17cc7 | |||
| 00fa7beebd | |||
| b9d13de591 | |||
| 7896890ffc | |||
| a2043abb51 | |||
| 64bb1b3bb5 | |||
| a1a56402d1 | |||
| 582d3774a4 | |||
| fab08cfaf8 | |||
| 550c48f29e | |||
| 0c64580abf | |||
| 0ad9b7e233 | |||
| b9bcc45cce | |||
| bcd285e368 | |||
| c0ad98eb34 | |||
| 178278235e | |||
| 11664956c7 | |||
| f020979188 | |||
| da5c8d6773 | |||
| 31051cd6c4 | |||
| c51058e6ac | |||
| 3ba913bb21 | |||
| 95116f70e8 | |||
| 5b282b62e7 | |||
| 5e74fce6e2 | |||
| 96b279b350 | |||
| fc4ad562ae | |||
| 480a8df31a |
@@ -1,4 +1,5 @@
|
|||||||
name: Docker Hub Workflow
|
name: Docker Hub Workflow
|
||||||
|
run-name: Docker Hub Workflow
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -48,9 +49,15 @@ jobs:
|
|||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||||
|
-
|
||||||
|
name: Run trivy scan
|
||||||
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
|
with:
|
||||||
|
docker-build-args: '--target backend-production -f Dockerfile'
|
||||||
|
docker-image-name: 'docker.io/lasuite/meet-backend:${{ github.sha }}'
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: backend-production
|
target: backend-production
|
||||||
@@ -92,9 +99,15 @@ jobs:
|
|||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||||
|
-
|
||||||
|
name: Run trivy scan
|
||||||
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
|
with:
|
||||||
|
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||||
|
docker-image-name: 'docker.io/lasuite/meet-frontend:${{ github.sha }}'
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./src/frontend/Dockerfile
|
file: ./src/frontend/Dockerfile
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Helmfile lint
|
||||||
|
run-name: Helmfile lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
helmfile-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ghcr.io/helmfile/helmfile:latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
uses: numerique-gouv/action-helmfile-lint@main
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
helmfile-src: "src/helm"
|
||||||
|
repositories: "meet,secrets"
|
||||||
@@ -8,3 +8,5 @@ creation_rules:
|
|||||||
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa #marie
|
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa #marie
|
||||||
- age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw #argocd
|
- age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw #argocd
|
||||||
- age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9 #manuuu
|
- age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9 #manuuu
|
||||||
|
- age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m # github-repo
|
||||||
|
- age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r # Samuel Paccoud
|
||||||
|
|||||||
+16
-25
@@ -1,15 +1,14 @@
|
|||||||
# Django Meet
|
# Django Meet
|
||||||
|
|
||||||
# ---- base image to inherit from ----
|
# ---- base image to inherit from ----
|
||||||
FROM python:3.10-slim-bullseye as base
|
FROM python:3.12.6-alpine3.20 as base
|
||||||
|
|
||||||
# Upgrade pip to its latest release to speed up dependencies installation
|
# Upgrade pip to its latest release to speed up dependencies installation
|
||||||
RUN python -m pip install --upgrade pip
|
RUN python -m pip install --upgrade pip setuptools
|
||||||
|
|
||||||
# Upgrade system packages to install security updates
|
# Upgrade system packages to install security updates
|
||||||
RUN apt-get update && \
|
RUN apk update && \
|
||||||
apt-get -y upgrade && \
|
apk upgrade
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# ---- Back-end builder image ----
|
# ---- Back-end builder image ----
|
||||||
FROM base as back-builder
|
FROM base as back-builder
|
||||||
@@ -38,12 +37,9 @@ RUN yarn install --frozen-lockfile && \
|
|||||||
FROM base as link-collector
|
FROM base as link-collector
|
||||||
ARG MEET_STATIC_ROOT=/data/static
|
ARG MEET_STATIC_ROOT=/data/static
|
||||||
|
|
||||||
# Install libpangocairo & rdfind
|
RUN apk add \
|
||||||
RUN apt-get update && \
|
pango \
|
||||||
apt-get install -y \
|
rdfind
|
||||||
libpangocairo-1.0-0 \
|
|
||||||
rdfind && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy installed python dependencies
|
# Copy installed python dependencies
|
||||||
COPY --from=back-builder /install /usr/local
|
COPY --from=back-builder /install /usr/local
|
||||||
@@ -66,17 +62,14 @@ FROM base as core
|
|||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Install required system libs
|
RUN apk add \
|
||||||
RUN apt-get update && \
|
gettext \
|
||||||
apt-get install -y \
|
cairo \
|
||||||
gettext \
|
libffi-dev \
|
||||||
libcairo2 \
|
gdk-pixbuf \
|
||||||
libffi-dev \
|
pango \
|
||||||
libgdk-pixbuf2.0-0 \
|
shared-mime-info
|
||||||
libpango-1.0-0 \
|
|
||||||
libpangocairo-1.0-0 \
|
|
||||||
shared-mime-info && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy entrypoint
|
# Copy entrypoint
|
||||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||||
@@ -106,9 +99,7 @@ FROM core as backend-development
|
|||||||
USER root:root
|
USER root:root
|
||||||
|
|
||||||
# Install psql
|
# Install psql
|
||||||
RUN apt-get update && \
|
RUN apk add postgresql-client
|
||||||
apt-get install -y postgresql-client && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Uninstall Meet and re-install it in editable mode along with development
|
# Uninstall Meet and re-install it in editable mode along with development
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HELMFILE=src/helm/helmfile.yaml
|
||||||
|
|
||||||
|
environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//')
|
||||||
|
|
||||||
|
for env in $environments; do
|
||||||
|
echo "################### $env lint ###################"
|
||||||
|
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
|
||||||
|
echo -e "\n"
|
||||||
|
done
|
||||||
+3
-1
@@ -13,7 +13,9 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"groupName": "ignored js dependencies",
|
"groupName": "ignored js dependencies",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"matchPackageNames": []
|
"matchPackageNames": [
|
||||||
|
"eslint"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule secrets updated: f5fbc16e6e...2ef2610071
@@ -0,0 +1,316 @@
|
|||||||
|
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from minio import Minio
|
||||||
|
from django.conf import settings
|
||||||
|
import openai
|
||||||
|
import logging
|
||||||
|
from ..models import Room, RoleChoices
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompt(transcript, date):
|
||||||
|
return f"""
|
||||||
|
|
||||||
|
Audience: Coworkers.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Detect the language of the transcript and provide your entire response in the same language.
|
||||||
|
- If any part of the transcript is unclear or lacks detail, politely inform the user, specifying which areas need further clarification.
|
||||||
|
- Ensure the accuracy of all information and refrain from adding unverified details.
|
||||||
|
- Format the response using proper markdown and structured sections.
|
||||||
|
- Be concise and avoid repeating yourself between the sections.
|
||||||
|
- Be super precise on nickname
|
||||||
|
- Be a nit-picker
|
||||||
|
- Auto-evaluate your response
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
- Write something your are not sure.
|
||||||
|
- Write something that is not mention in the transcript.
|
||||||
|
- Don't make mistake while mentioning someone
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Summarize the provided meeting transcript into clear and well-organized meeting minutes. The summary should be structured into the following sections, excluding irrelevant or inapplicable details:
|
||||||
|
|
||||||
|
1. **Summary**: Write a TL;DR of the meeting.
|
||||||
|
2. **Subjects Discussed**: List the key points or issues in bullet points.
|
||||||
|
4. **Next Steps**: Provide action items as bullet points, assigning each task to a responsible individual and including deadlines (if mentioned). Format action items as tickable checkboxes. Ensure every action is assigned and, if a deadline is provided, that it is clearly stated.
|
||||||
|
|
||||||
|
**Transcript**:
|
||||||
|
{transcript}
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
### Summary [Translate this title based on the transcript’s language]
|
||||||
|
[Provide a brief overview of the key points discussed]
|
||||||
|
|
||||||
|
### Subjects Discussed [Translate this title based on the transcript’s language]
|
||||||
|
- [Summarize each topic concisely]
|
||||||
|
|
||||||
|
### Next Steps [Translate this title based on the transcript’s language]
|
||||||
|
- [ ] Action item [Assign to the responsible individual(s) and include a deadline if applicable, follow this strict format: Action - List of owner(s), deadline.]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_room_and_owners(slug):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
room = Room.objects.get(slug=slug)
|
||||||
|
owner_accesses = room.accesses.filter(role=RoleChoices.OWNER)
|
||||||
|
owners = [access.user for access in owner_accesses]
|
||||||
|
|
||||||
|
logger.info("Room %s has owners: %s", slug, owners)
|
||||||
|
|
||||||
|
except Room.DoesNotExist:
|
||||||
|
logger.error("Room with slug %s does not exist", slug)
|
||||||
|
|
||||||
|
owners = None
|
||||||
|
room = None
|
||||||
|
|
||||||
|
return room, owners
|
||||||
|
|
||||||
|
|
||||||
|
def remove_temporary_file(path):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
os.remove(path)
|
||||||
|
logger.info("Temporary file %s has been deleted.", path)
|
||||||
|
|
||||||
|
def get_blocknote_content(summary):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
if not settings.BLOCKNOTE_CONVERTER_URL:
|
||||||
|
logger.error("BLOCKNOTE_CONVERTER_URL is not configured")
|
||||||
|
return None
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"markdown": summary
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Converting summary in BlockNote.js…")
|
||||||
|
response = requests.post(settings.BLOCKNOTE_CONVERTER_URL, headers=headers, json=data)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"Failed to convert summary. Status code: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if not 'content' in response_data:
|
||||||
|
logger.error(f"Content is missing: %s", response_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
content = response_data['content']
|
||||||
|
logger.info("Base64 content: %s", content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_document_link(content, email):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
logger.info("Create a document for %s", email)
|
||||||
|
|
||||||
|
if not settings.DOCS_BASE_URL:
|
||||||
|
logger.error("DOCS_BASE_URL is not configured")
|
||||||
|
return None
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"content": content,
|
||||||
|
"owner": email
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Querying docs…")
|
||||||
|
response = requests.post(f"{settings.DOCS_BASE_URL}/api/v1.0/summary/", headers=headers, json=data)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"Failed to get document's id. Status code: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if not 'id' in response_data:
|
||||||
|
logger.error(f"ID is missing: %s", response_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
id = response_data['id']
|
||||||
|
logger.info("Document's id: %s", id)
|
||||||
|
|
||||||
|
return f"{settings.DOCS_BASE_URL}/docs/{id}/"
|
||||||
|
|
||||||
|
|
||||||
|
def email_owner_with_summary(room, link, owner):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
logger.info("Emailing owner: %s", owner)
|
||||||
|
|
||||||
|
try:
|
||||||
|
room.email_summary(owners=[owner], link=link)
|
||||||
|
except smtplib.SMTPException:
|
||||||
|
logger.error("Error while emailing owner")
|
||||||
|
|
||||||
|
def strip_room_slug(filename):
|
||||||
|
"""Wip."""
|
||||||
|
return filename.split("_")[2].split(".")[0]
|
||||||
|
|
||||||
|
def strip_room_date(filename):
|
||||||
|
"""Wip."""
|
||||||
|
return filename.split("_")[1].split(".")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_minio_client():
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Minio(
|
||||||
|
settings.MINIO_URL,
|
||||||
|
access_key=settings.MINIO_ACCESS_KEY,
|
||||||
|
secret_key=settings.MINIO_SECRET_KEY,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("An error occurred while creating the Minio client %s: %s", settings.MINIO_URL, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def download_temporary_file(minio_client, filename):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
temp_file_path = None
|
||||||
|
|
||||||
|
logger.info('downloading file %s', filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
audio_file_stream = minio_client.get_object(settings.MINIO_BUCKET, object_name=filename)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file:
|
||||||
|
|
||||||
|
for data in audio_file_stream.stream(32 * 1024):
|
||||||
|
temp_audio_file.write(data)
|
||||||
|
|
||||||
|
temp_file_path = temp_audio_file.name
|
||||||
|
logger.info('Temporary file created at %s', temp_file_path)
|
||||||
|
|
||||||
|
audio_file_stream.close()
|
||||||
|
audio_file_stream.release_conn()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("An error occurred while accessing the object: %s", str(e))
|
||||||
|
|
||||||
|
return temp_file_path
|
||||||
|
|
||||||
|
|
||||||
|
# todo - discuss retry policy if the webhook fail
|
||||||
|
@api_view(["POST"])
|
||||||
|
def minio_webhook(request):
|
||||||
|
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
logger.info('Minio webhook sent %s', data)
|
||||||
|
|
||||||
|
record = data["Records"][0]
|
||||||
|
s3 = record['s3']
|
||||||
|
bucket = s3['bucket']
|
||||||
|
bucket_name = bucket['name']
|
||||||
|
object = s3['object']
|
||||||
|
filename = object['key']
|
||||||
|
|
||||||
|
if bucket_name != settings.MINIO_BUCKET:
|
||||||
|
logger.info('Not interested in this bucket: %s', bucket_name)
|
||||||
|
return Response("Not interested in this bucket")
|
||||||
|
|
||||||
|
if object['contentType'] != 'audio/ogg':
|
||||||
|
logger.info('Not interested in this file type: %s', object['contentType'])
|
||||||
|
return Response("Not interested in this file type")
|
||||||
|
|
||||||
|
room_slug = strip_room_slug(filename)
|
||||||
|
room_date = strip_room_date(filename)
|
||||||
|
logger.info('file received %s for room %s', filename, room_slug)
|
||||||
|
|
||||||
|
minio_client = get_minio_client()
|
||||||
|
|
||||||
|
temp_file_path = None
|
||||||
|
summary = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_file_path = download_temporary_file(minio_client, filename)
|
||||||
|
|
||||||
|
if settings.OPENAI_ENABLE and temp_file_path:
|
||||||
|
logger.info('Initiating OpenAI client …')
|
||||||
|
openai_client = openai.OpenAI(
|
||||||
|
api_key=settings.OPENAI_API_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(temp_file_path, "rb") as audio_file:
|
||||||
|
logger.info('Querying transcription …')
|
||||||
|
transcript = openai_client.audio.transcriptions.create(
|
||||||
|
model="whisper-1",
|
||||||
|
file=audio_file
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('Transcript: \n %s', transcript)
|
||||||
|
prompt = get_prompt(transcript.text, room_date)
|
||||||
|
|
||||||
|
logger.info('Prompt: \n %s', prompt)
|
||||||
|
|
||||||
|
summary_response = openai_client.chat.completions.create(
|
||||||
|
model="gpt-4o",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a concise and structured assistant, that summarizes meeting transcripts."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = summary_response.choices[0].message.content
|
||||||
|
logger.info('Summary: \n %s', summary)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("An error occurred: %s", str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
remove_temporary_file(temp_file_path)
|
||||||
|
|
||||||
|
if not summary:
|
||||||
|
logger.error("Empty summary.")
|
||||||
|
return Response("")
|
||||||
|
|
||||||
|
room, owners = get_room_and_owners(room_slug)
|
||||||
|
|
||||||
|
if not owners or not room:
|
||||||
|
logger.error("No owners in room %s", room_slug)
|
||||||
|
return Response("")
|
||||||
|
|
||||||
|
content = get_blocknote_content(summary)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
logger.error("Empty content.")
|
||||||
|
return Response("")
|
||||||
|
|
||||||
|
owner = owners[0]
|
||||||
|
|
||||||
|
link = get_document_link(content, owner.email)
|
||||||
|
|
||||||
|
if not link:
|
||||||
|
logger.error("Empty link.")
|
||||||
|
return Response("")
|
||||||
|
|
||||||
|
email_owner_with_summary(room, link, owner)
|
||||||
|
|
||||||
|
return Response("")
|
||||||
@@ -15,6 +15,8 @@ from django.utils.functional import lazy
|
|||||||
from django.utils.text import capfirst, slugify
|
from django.utils.text import capfirst, slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@@ -325,3 +327,24 @@ class Room(Resource):
|
|||||||
else:
|
else:
|
||||||
raise ValidationError({"name": f'Room name "{self.name:s}" is reserved.'})
|
raise ValidationError({"name": f'Room name "{self.name:s}" is reserved.'})
|
||||||
super().clean_fields(exclude=exclude)
|
super().clean_fields(exclude=exclude)
|
||||||
|
|
||||||
|
|
||||||
|
def email_summary(self, owners, link):
|
||||||
|
"""Wip"""
|
||||||
|
|
||||||
|
template_vars = {
|
||||||
|
"title": "Votre résumé est prêt",
|
||||||
|
"link": link,
|
||||||
|
"room": self.slug,
|
||||||
|
}
|
||||||
|
msg_html = render_to_string("mail/html/summary.html", template_vars)
|
||||||
|
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||||
|
|
||||||
|
for owner in owners:
|
||||||
|
owner.email_user(
|
||||||
|
subject="Votre résumé est prêt",
|
||||||
|
from_email=settings.EMAIL_FROM,
|
||||||
|
message=msg_plain,
|
||||||
|
html_message=msg_html,
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.urls import include, path
|
|||||||
|
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from core.api import viewsets
|
from core.api import get_frontend_configuration, viewsets, demo
|
||||||
from core.authentication.urls import urlpatterns as oidc_urls
|
from core.authentication.urls import urlpatterns as oidc_urls
|
||||||
|
|
||||||
# - Main endpoints
|
# - Main endpoints
|
||||||
@@ -23,6 +23,8 @@ urlpatterns = [
|
|||||||
[
|
[
|
||||||
*router.urls,
|
*router.urls,
|
||||||
*oidc_urls,
|
*oidc_urls,
|
||||||
|
path("config/", get_frontend_configuration, name="config"),
|
||||||
|
path("minio-webhook/", demo.minio_webhook, name="demo"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str:
|
|||||||
room=room,
|
room=room,
|
||||||
room_join=True,
|
room_join=True,
|
||||||
room_admin=True,
|
room_admin=True,
|
||||||
|
room_record=True,
|
||||||
can_update_own_metadata=True,
|
can_update_own_metadata=True,
|
||||||
can_publish_sources=[
|
can_publish_sources=[
|
||||||
"camera",
|
"camera",
|
||||||
|
|||||||
@@ -251,6 +251,19 @@ class Base(Configuration):
|
|||||||
"REDOC_DIST": "SIDECAR",
|
"REDOC_DIST": "SIDECAR",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
FRONTEND_CONFIGURATION = {
|
||||||
|
"analytics": values.DictValue(
|
||||||
|
{}, environ_name="FRONTEND_ANALYTICS", environ_prefix=None
|
||||||
|
),
|
||||||
|
"support": values.DictValue(
|
||||||
|
{}, environ_name="FRONTEND_SUPPORT", environ_prefix=None
|
||||||
|
),
|
||||||
|
"silence_livekit_debug_logs": values.BooleanValue(
|
||||||
|
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
# Mail
|
# Mail
|
||||||
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
||||||
EMAIL_HOST = values.Value(None)
|
EMAIL_HOST = values.Value(None)
|
||||||
@@ -258,6 +271,7 @@ class Base(Configuration):
|
|||||||
EMAIL_HOST_PASSWORD = values.Value(None)
|
EMAIL_HOST_PASSWORD = values.Value(None)
|
||||||
EMAIL_PORT = values.PositiveIntegerValue(None)
|
EMAIL_PORT = values.PositiveIntegerValue(None)
|
||||||
EMAIL_USE_TLS = values.BooleanValue(False)
|
EMAIL_USE_TLS = values.BooleanValue(False)
|
||||||
|
EMAIL_USE_SSL = values.BooleanValue(False)
|
||||||
EMAIL_FROM = values.Value("from@example.com")
|
EMAIL_FROM = values.Value("from@example.com")
|
||||||
|
|
||||||
AUTH_USER_MODEL = "core.User"
|
AUTH_USER_MODEL = "core.User"
|
||||||
@@ -372,6 +386,53 @@ class Base(Configuration):
|
|||||||
None, environ_name="ANALYTICS_KEY", environ_prefix=None
|
None, environ_name="ANALYTICS_KEY", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# todo - totally wip
|
||||||
|
AWS_S3_ENDPOINT_URL = values.Value(
|
||||||
|
environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None
|
||||||
|
)
|
||||||
|
AWS_S3_ACCESS_KEY_ID = values.Value(
|
||||||
|
environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None
|
||||||
|
)
|
||||||
|
AWS_S3_SECRET_ACCESS_KEY = values.Value(
|
||||||
|
environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None
|
||||||
|
)
|
||||||
|
AWS_S3_REGION_NAME = values.Value(
|
||||||
|
environ_name="AWS_S3_REGION_NAME", environ_prefix=None
|
||||||
|
)
|
||||||
|
AWS_STORAGE_BUCKET_NAME = values.Value(
|
||||||
|
"meet-media-storage",
|
||||||
|
environ_name="AWS_STORAGE_BUCKET_NAME",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
OPENAI_API_KEY = values.Value(
|
||||||
|
None, environ_name="OPENAI_API_KEY", environ_prefix=None
|
||||||
|
)
|
||||||
|
OPENAI_ENABLE = values.BooleanValue(
|
||||||
|
True, environ_name="OPENAI_ENABLE", environ_prefix=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# todo - totally wip
|
||||||
|
MINIO_ACCESS_KEY = values.Value(
|
||||||
|
None, environ_name="MINIO_ACCESS_KEY", environ_prefix=None
|
||||||
|
)
|
||||||
|
MINIO_SECRET_KEY = values.Value(
|
||||||
|
None, environ_name="MINIO_SECRET_KEY", environ_prefix=None
|
||||||
|
)
|
||||||
|
MINIO_URL = values.Value(
|
||||||
|
None, environ_name="MINIO_URL", environ_prefix=None
|
||||||
|
)
|
||||||
|
MINIO_BUCKET = values.Value(
|
||||||
|
'livekit-staging-livekit-egress', environ_name="MINIO_BUCKET", environ_prefix=None
|
||||||
|
)
|
||||||
|
|
||||||
|
BLOCKNOTE_CONVERTER_URL = values.Value(
|
||||||
|
'https://converter-blocknote.osc-fr1.scalingo.io/', environ_name="", environ_prefix=None
|
||||||
|
)
|
||||||
|
DOCS_BASE_URL = values.Value(
|
||||||
|
'https://docs-ia.beta.numerique.gouv.fr', environ_name="", environ_prefix=None
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@property
|
@property
|
||||||
def ENVIRONMENT(self):
|
def ENVIRONMENT(self):
|
||||||
@@ -515,6 +576,20 @@ class Production(Base):
|
|||||||
ALLOWED_HOSTS=["foo.com", "foo.fr"]
|
ALLOWED_HOSTS=["foo.com", "foo.fr"]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
*values.ListValue([], environ_name="ALLOWED_HOSTS"),
|
*values.ListValue([], environ_name="ALLOWED_HOSTS"),
|
||||||
@@ -535,6 +610,14 @@ class Production(Base):
|
|||||||
#
|
#
|
||||||
# In other cases, you should comment the following line to avoid security issues.
|
# In other cases, you should comment the following line to avoid security issues.
|
||||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
SECURE_HSTS_SECONDS = 60
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
SECURE_REDIRECT_EXEMPT = [
|
||||||
|
"^__lbheartbeat__",
|
||||||
|
"^__heartbeat__",
|
||||||
|
]
|
||||||
|
|
||||||
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
|
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|||||||
+12
-10
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.35.10",
|
"boto3==1.35.19",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.1.0",
|
||||||
"celery[redis]==5.4.0",
|
"celery[redis]==5.4.0",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
@@ -36,11 +36,11 @@ dependencies = [
|
|||||||
"django-redis==5.4.0",
|
"django-redis==5.4.0",
|
||||||
"django-storages[s3]==1.14.4",
|
"django-storages[s3]==1.14.4",
|
||||||
"django-timezone-field>=5.1",
|
"django-timezone-field>=5.1",
|
||||||
"django==5.1",
|
"django==5.1.1",
|
||||||
"djangorestframework==3.15.2",
|
"djangorestframework==3.15.2",
|
||||||
"drf_spectacular==0.27.2",
|
"drf_spectacular==0.27.2",
|
||||||
"dockerflow==2024.4.2",
|
"dockerflow==2024.4.2",
|
||||||
"easy_thumbnails==2.9",
|
"easy_thumbnails==2.10",
|
||||||
"factory_boy==3.3.1",
|
"factory_boy==3.3.1",
|
||||||
"freezegun==1.5.1",
|
"freezegun==1.5.1",
|
||||||
"gunicorn==23.0.0",
|
"gunicorn==23.0.0",
|
||||||
@@ -48,16 +48,18 @@ dependencies = [
|
|||||||
"june-analytics-python==2.3.0",
|
"june-analytics-python==2.3.0",
|
||||||
"markdown==3.7",
|
"markdown==3.7",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.5.0",
|
||||||
"psycopg[binary]==3.2.1",
|
"psycopg[binary]==3.2.2",
|
||||||
"PyJWT==2.9.0",
|
"PyJWT==2.9.0",
|
||||||
"python-frontmatter==1.1.0",
|
"python-frontmatter==1.1.0",
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
"sentry-sdk==2.13.0",
|
"sentry-sdk==2.14.0",
|
||||||
"url-normalize==1.4.3",
|
"url-normalize==1.4.3",
|
||||||
"WeasyPrint>=60.2",
|
"WeasyPrint>=60.2",
|
||||||
"whitenoise==6.7.0",
|
"whitenoise==6.7.0",
|
||||||
"mozilla-django-oidc==4.0.1",
|
"mozilla-django-oidc==4.0.1",
|
||||||
"livekit-api==0.7.0",
|
"livekit-api==0.7.0",
|
||||||
|
"minio==7.2.9",
|
||||||
|
"openai==1.51.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -76,13 +78,13 @@ dev = [
|
|||||||
"pylint-django==2.5.5",
|
"pylint-django==2.5.5",
|
||||||
"pylint==3.2.7",
|
"pylint==3.2.7",
|
||||||
"pytest-cov==5.0.0",
|
"pytest-cov==5.0.0",
|
||||||
"pytest-django==4.8.0",
|
"pytest-django==4.9.0",
|
||||||
"pytest==8.3.2",
|
"pytest==8.3.3",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.6.1",
|
"pytest-xdist==3.6.1",
|
||||||
"responses==0.25.3",
|
"responses==0.25.3",
|
||||||
"ruff==0.6.3",
|
"ruff==0.6.5",
|
||||||
"types-requests==2.32.0.20240712",
|
"types-requests==2.32.0.20240914",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ WORKDIR /home/frontend
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ---- Front-end image ----
|
# ---- Front-end image ----
|
||||||
FROM nginxinc/nginx-unprivileged:1.25 as frontend-production
|
FROM nginxinc/nginx-unprivileged:1.26-alpine as frontend-production
|
||||||
|
|
||||||
# Un-privileged user running the application
|
# Un-privileged user running the application
|
||||||
ARG DOCKER_USER
|
ARG DOCKER_USER
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Meet</title>
|
<title>Visio</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+2556
-2843
File diff suppressed because it is too large
Load Diff
+32
-29
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.5",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
@@ -13,45 +13,48 @@
|
|||||||
"check": "prettier --check ./src"
|
"check": "prettier --check ./src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/components-react": "2.3.3",
|
"@livekit/components-react": "2.6.5",
|
||||||
"@livekit/components-styles": "1.0.12",
|
"@livekit/components-styles": "1.1.3",
|
||||||
"@pandacss/preset-panda": "0.41.0",
|
"@livekit/track-processors": "0.3.2",
|
||||||
"@react-aria/toast": "3.0.0-beta.15",
|
"@pandacss/preset-panda": "0.46.1",
|
||||||
|
"@react-aria/toast": "3.0.0-beta.16",
|
||||||
"@remixicon/react": "4.2.0",
|
"@remixicon/react": "4.2.0",
|
||||||
"@tanstack/react-query": "5.49.2",
|
"@tanstack/react-query": "5.59.4",
|
||||||
|
"crisp-sdk-web": "1.0.25",
|
||||||
"hoofd": "1.7.1",
|
"hoofd": "1.7.1",
|
||||||
"i18next": "23.12.1",
|
"i18next": "23.15.2",
|
||||||
"i18next-browser-languagedetector": "8.0.0",
|
"i18next-browser-languagedetector": "8.0.0",
|
||||||
"i18next-parser": "9.0.0",
|
"i18next-parser": "9.0.2",
|
||||||
"i18next-resources-to-backend": "1.2.1",
|
"i18next-resources-to-backend": "1.2.1",
|
||||||
"livekit-client": "2.3.1",
|
"livekit-client": "2.5.7",
|
||||||
"react": "18.2.0",
|
"posthog-js": "1.167.0",
|
||||||
"react-aria-components": "1.2.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-aria-components": "1.4.0",
|
||||||
"react-i18next": "14.1.3",
|
"react-dom": "18.3.1",
|
||||||
|
"react-i18next": "15.0.2",
|
||||||
"use-sound": "4.0.3",
|
"use-sound": "4.0.3",
|
||||||
"valtio": "1.13.2",
|
"valtio": "2.0.0",
|
||||||
"wouter": "3.3.0"
|
"wouter": "3.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pandacss/dev": "0.41.0",
|
"@pandacss/dev": "0.46.1",
|
||||||
"@tanstack/eslint-plugin-query": "5.49.1",
|
"@tanstack/eslint-plugin-query": "5.59.2",
|
||||||
"@tanstack/react-query-devtools": "5.49.2",
|
"@tanstack/react-query-devtools": "5.59.4",
|
||||||
"@types/node": "20.14.9",
|
"@types/node": "20.16.11",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.11",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
"@typescript-eslint/eslint-plugin": "8.8.1",
|
||||||
"@typescript-eslint/parser": "7.13.1",
|
"@typescript-eslint/parser": "8.8.1",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.2",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
"eslint-plugin-jsx-a11y": "6.10.0",
|
||||||
"eslint-plugin-react-hooks": "4.6.2",
|
"eslint-plugin-react-hooks": "4.6.2",
|
||||||
"eslint-plugin-react-refresh": "0.4.7",
|
"eslint-plugin-react-refresh": "0.4.12",
|
||||||
"postcss": "8.4.39",
|
"postcss": "8.4.47",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"typescript": "5.5.2",
|
"typescript": "5.6.3",
|
||||||
"vite": "5.3.1",
|
"vite": "5.4.8",
|
||||||
"vite-tsconfig-paths": "4.3.2"
|
"vite-tsconfig-paths": "5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,16 +64,16 @@ const config: Config = {
|
|||||||
'100%': { boxShadow: '0 0 0 0 rgba(255, 255, 255, 0)' },
|
'100%': { boxShadow: '0 0 0 0 rgba(255, 255, 255, 0)' },
|
||||||
},
|
},
|
||||||
active_speaker: {
|
active_speaker: {
|
||||||
'0%': { height: '4px' },
|
'0%': { height: '25%' },
|
||||||
'25%': { height: '8px' },
|
'25%': { height: '45%' },
|
||||||
'50%': { height: '6px' },
|
'50%': { height: '20%' },
|
||||||
'100%': { height: '16px' },
|
'100%': { height: '55%' },
|
||||||
},
|
},
|
||||||
active_speake_small: {
|
active_speaker_small: {
|
||||||
'0%': { height: '4px' },
|
'0%': { height: '20%' },
|
||||||
'25%': { height: '6px' },
|
'25%': { height: '25%' },
|
||||||
'50%': { height: '4px' },
|
'50%': { height: '18%' },
|
||||||
'100%': { height: '8px' },
|
'100%': { height: '25%' },
|
||||||
},
|
},
|
||||||
wave_hand: {
|
wave_hand: {
|
||||||
'0%': { transform: 'rotate(0deg)' },
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
|||||||
@@ -11,18 +11,15 @@ import { Layout } from './layout/Layout'
|
|||||||
import { NotFoundScreen } from './components/NotFoundScreen'
|
import { NotFoundScreen } from './components/NotFoundScreen'
|
||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
import './i18n/init'
|
import './i18n/init'
|
||||||
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
|
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
|
import { AppInitialization } from '@/components/AppInitialization'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
useLang(i18n.language)
|
useLang(i18n.language)
|
||||||
|
|
||||||
const isProduction = import.meta.env.PROD
|
|
||||||
silenceLiveKitLogs(isProduction)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AppInitialization />
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<I18nProvider locale={i18n.language}>
|
<I18nProvider locale={i18n.language}>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const keys = {
|
export const keys = {
|
||||||
user: 'user',
|
user: 'user',
|
||||||
room: 'room',
|
room: 'room',
|
||||||
|
config: 'config',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { fetchApi } from './fetchApi'
|
||||||
|
import { keys } from './queryKeys'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export interface ApiConfig {
|
||||||
|
analytics?: {
|
||||||
|
id: string
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
support?: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
silence_livekit_debug_logs?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchConfig = (): Promise<ApiConfig> => {
|
||||||
|
return fetchApi<ApiConfig>(`config/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useConfig = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [keys.config],
|
||||||
|
queryFn: fetchConfig,
|
||||||
|
staleTime: Infinity,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { silenceLiveKitLogs } from '@/utils/livekit'
|
||||||
|
import { useConfig } from '@/api/useConfig'
|
||||||
|
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||||
|
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||||
|
|
||||||
|
export const AppInitialization = () => {
|
||||||
|
const { data } = useConfig()
|
||||||
|
|
||||||
|
const {
|
||||||
|
analytics = {},
|
||||||
|
support = {},
|
||||||
|
silence_livekit_debug_logs = false,
|
||||||
|
} = data || {}
|
||||||
|
|
||||||
|
useAnalytics(analytics)
|
||||||
|
useSupport(support)
|
||||||
|
silenceLiveKitLogs(silence_livekit_debug_logs)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Button } from '@/primitives'
|
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { RiExternalLinkLine } from '@remixicon/react'
|
import { RiExternalLinkLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { LinkButton } from '@/primitives'
|
||||||
|
|
||||||
export const Feedback = () => {
|
export const Feedback = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Button
|
<LinkButton
|
||||||
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||||
variant="success"
|
variant="success"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -20,6 +20,6 @@ export const Feedback = () => {
|
|||||||
className={css({ marginLeft: 0.5 })}
|
className={css({ marginLeft: 0.5 })}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</LinkButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useLocation } from 'wouter'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
|
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
|
|
||||||
|
export const startAnalyticsSession = (data: ApiUser) => {
|
||||||
|
if (posthog._isIdentified()) return
|
||||||
|
const { id, email } = data
|
||||||
|
posthog.identify(id, { email })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const terminateAnalyticsSession = () => {
|
||||||
|
if (!posthog._isIdentified()) return
|
||||||
|
posthog.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type useAnalyticsProps = {
|
||||||
|
id?: string
|
||||||
|
host?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
|
||||||
|
const [location] = useLocation()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id || !host) return
|
||||||
|
if (posthog.__loaded) return
|
||||||
|
posthog.init(id, {
|
||||||
|
api_host: host,
|
||||||
|
person_profiles: 'always',
|
||||||
|
})
|
||||||
|
}, [id, host])
|
||||||
|
|
||||||
|
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
|
||||||
|
useEffect(() => {
|
||||||
|
posthog.capture('$pageview')
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export const fetchUser = (): Promise<ApiUser | false> => {
|
|||||||
// make sure to not resolve the promise while trying to silent login
|
// make sure to not resolve the promise while trying to silent login
|
||||||
// so that consumers of fetchUser don't think the work already ended
|
// so that consumers of fetchUser don't think the work already ended
|
||||||
if (canAttemptSilentLogin()) {
|
if (canAttemptSilentLogin()) {
|
||||||
attemptSilentLogin(3600)
|
attemptSilentLogin(300)
|
||||||
} else {
|
} else {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { fetchUser } from './fetchUser'
|
import { fetchUser } from './fetchUser'
|
||||||
import { type ApiUser } from './ApiUser'
|
import { type ApiUser } from './ApiUser'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { startAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
|
||||||
|
import { initializeSupportSession } from '@/features/support/hooks/useSupport'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns info about currently logged in user
|
* returns info about currently logged-in user
|
||||||
*
|
*
|
||||||
* `isLoggedIn` is undefined while query is loading and true/false when it's done
|
* `isLoggedIn` is undefined while query is loading and true/false when it's done
|
||||||
*/
|
*/
|
||||||
@@ -12,9 +15,16 @@ export const useUser = () => {
|
|||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [keys.user],
|
queryKey: [keys.user],
|
||||||
queryFn: fetchUser,
|
queryFn: fetchUser,
|
||||||
staleTime: 1000 * 60 * 60, // 1 hour
|
staleTime: Infinity,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query?.data) {
|
||||||
|
startAnalyticsSession(query.data)
|
||||||
|
initializeSupportSession(query.data)
|
||||||
|
}
|
||||||
|
}, [query.data])
|
||||||
|
|
||||||
const isLoggedIn =
|
const isLoggedIn =
|
||||||
query.status === 'success' ? query.data !== false : undefined
|
query.status === 'success' ? query.data !== false : undefined
|
||||||
const isLoggedOut = isLoggedIn === false
|
const isLoggedOut = isLoggedIn === false
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
|
|||||||
name="roomId"
|
name="roomId"
|
||||||
label={t('joinInputLabel')}
|
label={t('joinInputLabel')}
|
||||||
description={t('joinInputExample', {
|
description={t('joinInputExample', {
|
||||||
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
|
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
|
||||||
})}
|
})}
|
||||||
validate={(value) => {
|
validate={(value) => {
|
||||||
return !isRoomValid(value.trim()) ? (
|
return !isRoomValid(value.trim()) ? (
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const LaterMeetingDialog = ({
|
|||||||
setIsCopied(true)
|
setIsCopied(true)
|
||||||
}}
|
}}
|
||||||
onHoverChange={setIsHovered}
|
onHoverChange={setIsHovered}
|
||||||
|
data-attr="later-dialog-copy"
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { DialogTrigger } from 'react-aria-components'
|
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
|
||||||
import { Button, Menu, Text } from '@/primitives'
|
import { Button, Menu, Text } from '@/primitives'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { Centered } from '@/layout/Centered'
|
import { Centered } from '@/layout/Centered'
|
||||||
import { generateRoomId } from '@/features/rooms'
|
import { generateRoomId } from '@/features/rooms'
|
||||||
import { authUrl, useUser, UserAware } from '@/features/auth'
|
import { useUser, UserAware } from '@/features/auth'
|
||||||
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
import { useCreateRoom } from '@/features/rooms'
|
import { useCreateRoom } from '@/features/rooms'
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||||
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
||||||
import { RiAddLine, RiLink } from '@remixicon/react'
|
import { RiAddLine, RiLink } from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
|
|
||||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
@@ -42,10 +42,12 @@ export const Home = () => {
|
|||||||
{t('loginToCreateMeeting')}
|
{t('loginToCreateMeeting')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<HStack gap="gutter">
|
<HStack gap="gutter" alignItems="start">
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Button variant="primary">{t('createMeeting')}</Button>
|
<Button variant="primary" data-attr="create-meeting">
|
||||||
|
{t('createMeeting')}
|
||||||
|
</Button>
|
||||||
<RACMenu>
|
<RACMenu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuItemRecipe({ icon: true })}
|
className={menuItemRecipe({ icon: true })}
|
||||||
@@ -57,6 +59,7 @@ export const Home = () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
data-attr="create-option-instant"
|
||||||
>
|
>
|
||||||
<RiAddLine size={18} />
|
<RiAddLine size={18} />
|
||||||
{t('createMenu.instantOption')}
|
{t('createMenu.instantOption')}
|
||||||
@@ -69,6 +72,7 @@ export const Home = () => {
|
|||||||
setLaterRoomId(data.slug)
|
setLaterRoomId(data.slug)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
data-attr="create-option-later"
|
||||||
>
|
>
|
||||||
<RiLink size={18} />
|
<RiLink size={18} />
|
||||||
{t('createMenu.laterOption')}
|
{t('createMenu.laterOption')}
|
||||||
@@ -76,12 +80,16 @@ export const Home = () => {
|
|||||||
</RACMenu>
|
</RACMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="primary" href={authUrl()}>
|
<ProConnectButton />
|
||||||
{t('login', { ns: 'global' })}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="primary" outline>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
outline
|
||||||
|
style={{
|
||||||
|
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('joinMeeting')}
|
{t('joinMeeting')}
|
||||||
</Button>
|
</Button>
|
||||||
<JoinMeetingDialog />
|
<JoinMeetingDialog />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
|
import { Participant, RoomEvent } from 'livekit-client'
|
||||||
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
||||||
import { NotificationType } from './NotificationType'
|
import { NotificationType } from './NotificationType'
|
||||||
import { Div } from '@/primitives'
|
import { Div } from '@/primitives'
|
||||||
@@ -34,47 +34,48 @@ export const MainNotificationToast = () => {
|
|||||||
}, [room, triggerNotificationSound])
|
}, [room, triggerNotificationSound])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeJoinNotification = (participant: Participant) => {
|
const removeParticipantNotifications = (participant: Participant) => {
|
||||||
const existingToast = toastQueue.visibleToasts.find(
|
toastQueue.visibleToasts.forEach((toast) => {
|
||||||
(toast) =>
|
if (toast.content.participant === participant) {
|
||||||
toast.content.participant === participant &&
|
toastQueue.close(toast.key)
|
||||||
toast.content.type === NotificationType.Joined
|
}
|
||||||
)
|
})
|
||||||
if (existingToast) {
|
|
||||||
toastQueue.close(existingToast.key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
room.on(RoomEvent.ParticipantDisconnected, removeJoinNotification)
|
room.on(RoomEvent.ParticipantDisconnected, removeParticipantNotifications)
|
||||||
return () => {
|
return () => {
|
||||||
room.off(RoomEvent.ParticipantConnected, removeJoinNotification)
|
room.off(
|
||||||
|
RoomEvent.ParticipantDisconnected,
|
||||||
|
removeParticipantNotifications
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [room])
|
}, [room])
|
||||||
|
|
||||||
// fixme - close all related toasters when hands are lowered remotely
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const decoder = new TextDecoder()
|
|
||||||
|
|
||||||
const handleNotificationReceived = (
|
const handleNotificationReceived = (
|
||||||
payload: Uint8Array,
|
prevMetadataStr: string | undefined,
|
||||||
participant?: RemoteParticipant
|
participant: Participant
|
||||||
) => {
|
) => {
|
||||||
if (!participant) {
|
if (!participant) return
|
||||||
return
|
if (isMobileBrowser()) return
|
||||||
}
|
if (participant.isLocal) return
|
||||||
if (isMobileBrowser()) {
|
|
||||||
return
|
const prevMetadata = JSON.parse(prevMetadataStr || '{}')
|
||||||
}
|
const metadata = JSON.parse(participant.metadata || '{}')
|
||||||
const notification = decoder.decode(payload)
|
|
||||||
|
if (prevMetadata.raised == metadata.raised) return
|
||||||
|
|
||||||
const existingToast = toastQueue.visibleToasts.find(
|
const existingToast = toastQueue.visibleToasts.find(
|
||||||
(toast) =>
|
(toast) =>
|
||||||
toast.content.participant === participant &&
|
toast.content.participant === participant &&
|
||||||
toast.content.type === NotificationType.Raised
|
toast.content.type === NotificationType.Raised
|
||||||
)
|
)
|
||||||
if (existingToast && notification === NotificationType.Lowered) {
|
|
||||||
|
if (existingToast && prevMetadata.raised && !metadata.raised) {
|
||||||
toastQueue.close(existingToast.key)
|
toastQueue.close(existingToast.key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!existingToast && notification === NotificationType.Raised) {
|
|
||||||
|
if (!existingToast && !prevMetadata.raised && metadata.raised) {
|
||||||
triggerNotificationSound(NotificationType.Raised)
|
triggerNotificationSound(NotificationType.Raised)
|
||||||
toastQueue.add(
|
toastQueue.add(
|
||||||
{
|
{
|
||||||
@@ -86,15 +87,25 @@ export const MainNotificationToast = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
room.on(RoomEvent.DataReceived, handleNotificationReceived)
|
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
room.off(RoomEvent.DataReceived, handleNotificationReceived)
|
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
|
||||||
}
|
}
|
||||||
}, [room, triggerNotificationSound])
|
}, [room, triggerNotificationSound])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const closeAllToasts = () => {
|
||||||
|
toastQueue.visibleToasts.forEach(({ key }) => toastQueue.close(key))
|
||||||
|
}
|
||||||
|
room.on(RoomEvent.Disconnected, closeAllToasts)
|
||||||
|
return () => {
|
||||||
|
room.off(RoomEvent.Disconnected, closeAllToasts)
|
||||||
|
}
|
||||||
|
}, [room])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
|
<Div position="absolute" bottom={0} right={5} zIndex={1000}>
|
||||||
<ToastProvider />
|
<ToastProvider />
|
||||||
</Div>
|
</Div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { HStack } from '@/styled-system/jsx'
|
|||||||
import { Button, Div } from '@/primitives'
|
import { Button, Div } from '@/primitives'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiCloseLine, RiHand } from '@remixicon/react'
|
import { RiCloseLine, RiHand } from '@remixicon/react'
|
||||||
import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction'
|
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||||
|
|
||||||
export function ToastRaised({ state, ...props }: ToastProps) {
|
export function ToastRaised({ state, ...props }: ToastProps) {
|
||||||
const { t } = useTranslation('notifications')
|
const { t } = useTranslation('notifications')
|
||||||
@@ -17,7 +17,7 @@ export function ToastRaised({ state, ...props }: ToastProps) {
|
|||||||
ref
|
ref
|
||||||
)
|
)
|
||||||
const participant = props.toast.content.participant
|
const participant = props.toast.content.participant
|
||||||
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
|
const { isParticipantsOpen, toggleParticipants } = useSidePanel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledToastContainer {...toastProps} ref={ref}>
|
<StyledToastContainer {...toastProps} ref={ref}>
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ const StyledContainer = styled('div', {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '2px',
|
gap: '2px',
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
pushToTalk: {
|
||||||
|
true: {
|
||||||
|
height: '46px',
|
||||||
|
width: '58px',
|
||||||
|
borderLeftRadius: 8,
|
||||||
|
borderRightRadius: 0,
|
||||||
|
backgroundColor: '#dbeafe',
|
||||||
|
border: '1px solid #3b82f6',
|
||||||
|
gap: '3px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledChild = styled('div', {
|
const StyledChild = styled('div', {
|
||||||
@@ -36,16 +49,32 @@ const StyledChild = styled('div', {
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: {
|
small: {
|
||||||
animationName: 'active_speake_small',
|
animationName: 'active_speaker_small',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pushToTalk: {
|
||||||
|
true: {
|
||||||
|
backgroundColor: 'primary',
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ActiveSpeaker = ({ isSpeaking }: { isSpeaking: boolean }) => {
|
export type ActiveSpeakerProps = {
|
||||||
|
isSpeaking: boolean
|
||||||
|
pushToTalk?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActiveSpeaker = ({
|
||||||
|
isSpeaking,
|
||||||
|
pushToTalk,
|
||||||
|
}: ActiveSpeakerProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer pushToTalk={pushToTalk}>
|
||||||
<StyledChild
|
<StyledChild
|
||||||
|
pushToTalk={pushToTalk}
|
||||||
active={isSpeaking}
|
active={isSpeaking}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
@@ -53,12 +82,14 @@ export const ActiveSpeaker = ({ isSpeaking }: { isSpeaking: boolean }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<StyledChild
|
<StyledChild
|
||||||
|
pushToTalk={pushToTalk}
|
||||||
active={isSpeaking}
|
active={isSpeaking}
|
||||||
style={{
|
style={{
|
||||||
animationDelay: '100ms',
|
animationDelay: '100ms',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<StyledChild
|
<StyledChild
|
||||||
|
pushToTalk={pushToTalk}
|
||||||
active={isSpeaking}
|
active={isSpeaking}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import { LiveKitRoom, type LocalUserChoices } from '@livekit/components-react'
|
||||||
formatChatMessageLinks,
|
|
||||||
LiveKitRoom,
|
|
||||||
type LocalUserChoices,
|
|
||||||
} from '@livekit/components-react'
|
|
||||||
import { Room, RoomOptions } from 'livekit-client'
|
import { Room, RoomOptions } from 'livekit-client'
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { QueryAware } from '@/components/QueryAware'
|
import { QueryAware } from '@/components/QueryAware'
|
||||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||||
@@ -19,6 +14,7 @@ import { useCreateRoom } from '../api/createRoom'
|
|||||||
import { InviteDialog } from './InviteDialog'
|
import { InviteDialog } from './InviteDialog'
|
||||||
|
|
||||||
import { VideoConference } from '../livekit/prefabs/VideoConference'
|
import { VideoConference } from '../livekit/prefabs/VideoConference'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -31,6 +27,9 @@ export const Conference = ({
|
|||||||
mode?: 'join' | 'create'
|
mode?: 'join' | 'create'
|
||||||
initialRoomData?: ApiRoom
|
initialRoomData?: ApiRoom
|
||||||
}) => {
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
posthog.capture('visit-room', { slug: roomId })
|
||||||
|
}, [roomId])
|
||||||
const fetchKey = [keys.room, roomId, userConfig.username]
|
const fetchKey = [keys.room, roomId, userConfig.username]
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -79,25 +78,6 @@ export const Conference = ({
|
|||||||
|
|
||||||
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
||||||
|
|
||||||
/**
|
|
||||||
* checks for actual click on the leave button instead of
|
|
||||||
* relying on LiveKitRoom onDisconnected because onDisconnected
|
|
||||||
* triggers even on page reload, it's not a user "onLeave" event really.
|
|
||||||
* Here we want to react to the user actually deciding to leave.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
const checkOnLeaveClick = (event: MouseEvent) => {
|
|
||||||
const target = event.target as HTMLElement
|
|
||||||
if (target.classList.contains('lk-disconnect-button')) {
|
|
||||||
navigateTo('feedback')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.body.addEventListener('click', checkOnLeaveClick)
|
|
||||||
return () => {
|
|
||||||
document.body.removeEventListener('click', checkOnLeaveClick)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
if (isCreateError) {
|
if (isCreateError) {
|
||||||
// this error screen should be replaced by a proper waiting room for anonymous user.
|
// this error screen should be replaced by a proper waiting room for anonymous user.
|
||||||
@@ -120,7 +100,7 @@ export const Conference = ({
|
|||||||
audio={userConfig.audioEnabled}
|
audio={userConfig.audioEnabled}
|
||||||
video={userConfig.videoEnabled}
|
video={userConfig.videoEnabled}
|
||||||
>
|
>
|
||||||
<VideoConference chatMessageFormatter={formatChatMessageLinks} />
|
<VideoConference />
|
||||||
{showInviteDialog && (
|
{showInviteDialog && (
|
||||||
<InviteDialog
|
<InviteDialog
|
||||||
isOpen={showInviteDialog}
|
isOpen={showInviteDialog}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const InviteDialog = ({
|
|||||||
setIsCopied(true)
|
setIsCopied(true)
|
||||||
}}
|
}}
|
||||||
onHoverChange={setIsHovered}
|
onHoverChange={setIsHovered}
|
||||||
|
data-attr="share-dialog-copy"
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export const Join = ({
|
|||||||
<PreJoin
|
<PreJoin
|
||||||
persistUserChoices
|
persistUserChoices
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
micLabel={t('join.micLabel')}
|
micLabel={t('join.audioinput.label')}
|
||||||
camLabel={t('join.camlabel')}
|
camLabel={t('join.videoinput.label')}
|
||||||
joinLabel={t('join.joinLabel')}
|
joinLabel={t('join.joinLabel')}
|
||||||
userLabel={t('join.userLabel')}
|
userLabel={t('join.userLabel')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { RoomEvent } from 'livekit-client'
|
||||||
|
import { egressStore } from '@/stores/egress.ts'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
export const RecordingIndicator = () => {
|
||||||
|
const room = useRoomContext()
|
||||||
|
const [isRecording, setIsRecording] = useState(room.isRecording)
|
||||||
|
|
||||||
|
const egressSnap = useSnapshot(egressStore)
|
||||||
|
const egressIsStopping = egressSnap.egressIsStopping
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRecordingStatusChanges = (isRecording: boolean) => {
|
||||||
|
if (!isRecording) {
|
||||||
|
egressStore.egressIsStopping = false
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRecording(isRecording)
|
||||||
|
}
|
||||||
|
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
|
||||||
|
return () => {
|
||||||
|
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
|
||||||
|
}
|
||||||
|
}, [room])
|
||||||
|
|
||||||
|
const getStatus = () => {
|
||||||
|
if (egressIsStopping) {
|
||||||
|
return 'saving recording'
|
||||||
|
}
|
||||||
|
if (isRecording) {
|
||||||
|
return 'recording'
|
||||||
|
}
|
||||||
|
if (!isRecording) {
|
||||||
|
return 'available'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Room status: {getStatus()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export const useLowerHandParticipants = () => {
|
|||||||
)
|
)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('An error occurred while lowering hands :', error)
|
||||||
throw new Error('An error occurred while lowering hands.')
|
throw new Error('An error occurred while lowering hands.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { fetchServerApi } from './fetchServerApi'
|
||||||
|
import { buildServerApiUrl } from './buildServerApiUrl'
|
||||||
|
import { useRoomData } from '../hooks/useRoomData'
|
||||||
|
import { useParams } from 'wouter'
|
||||||
|
|
||||||
|
export const useRecordRoom = () => {
|
||||||
|
const data = useRoomData()
|
||||||
|
const { roomId: roomSlug } = useParams()
|
||||||
|
|
||||||
|
const recordRoom = () => {
|
||||||
|
if (!data || !data?.livekit) {
|
||||||
|
throw new Error('Room data is not available')
|
||||||
|
}
|
||||||
|
if (!roomSlug) {
|
||||||
|
throw new Error('Room ID is not available')
|
||||||
|
}
|
||||||
|
return fetchServerApi(
|
||||||
|
buildServerApiUrl(
|
||||||
|
data.livekit.url,
|
||||||
|
'/twirp/livekit.Egress/StartRoomCompositeEgress'
|
||||||
|
),
|
||||||
|
data.livekit.token,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_name: data.livekit.room,
|
||||||
|
audio_only: true,
|
||||||
|
file_outputs: [
|
||||||
|
{
|
||||||
|
file_extension: 'ogg',
|
||||||
|
filepath: `{room_name}_{time}_${roomSlug}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRecordingRoom = (egressId: string) => {
|
||||||
|
if (!data || !data?.livekit) {
|
||||||
|
throw new Error('Room data is not available')
|
||||||
|
}
|
||||||
|
return fetchServerApi(
|
||||||
|
buildServerApiUrl(data.livekit.url, '/twirp/livekit.Egress/StopEgress'),
|
||||||
|
data.livekit.token,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
egressId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { recordRoom, stopRecordingRoom }
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useLocalParticipant } from '@livekit/components-react'
|
||||||
|
import { LocalVideoTrack } from 'livekit-client'
|
||||||
|
import { Text, P, ToggleButton, Div, H } from '@/primitives'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { HStack, styled, VStack } from '@/styled-system/jsx'
|
||||||
|
import {
|
||||||
|
BackgroundBlur,
|
||||||
|
BackgroundOptions,
|
||||||
|
ProcessorWrapper,
|
||||||
|
BackgroundTransformer,
|
||||||
|
} from '@livekit/track-processors'
|
||||||
|
|
||||||
|
const Information = styled('div', {
|
||||||
|
base: {
|
||||||
|
backgroundColor: 'orange.50',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '0.75rem 0.75rem',
|
||||||
|
marginTop: '0.8rem',
|
||||||
|
alignItems: 'start',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
enum BlurRadius {
|
||||||
|
NONE = 0,
|
||||||
|
LIGHT = 5,
|
||||||
|
NORMAL = 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Effects = () => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
|
||||||
|
const { isCameraEnabled, cameraTrack, localParticipant } =
|
||||||
|
useLocalParticipant()
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const [processorPending, setProcessorPending] = useState(false)
|
||||||
|
|
||||||
|
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
|
||||||
|
|
||||||
|
const getProcessor = () => {
|
||||||
|
return localCameraTrack?.getProcessor() as ProcessorWrapper<BackgroundOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBlurRadius = (): BlurRadius => {
|
||||||
|
const processor = getProcessor()
|
||||||
|
return (
|
||||||
|
(processor?.transformer as BackgroundTransformer)?.options?.blurRadius ||
|
||||||
|
BlurRadius.NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBlur = async (blurRadius: number) => {
|
||||||
|
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true)
|
||||||
|
if (!localCameraTrack) return
|
||||||
|
|
||||||
|
setProcessorPending(true)
|
||||||
|
|
||||||
|
const processor = getProcessor()
|
||||||
|
const currentBlurRadius = getBlurRadius()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (blurRadius == currentBlurRadius && processor) {
|
||||||
|
await localCameraTrack.stopProcessor()
|
||||||
|
} else if (!processor) {
|
||||||
|
await localCameraTrack.setProcessor(BackgroundBlur(blurRadius))
|
||||||
|
} else {
|
||||||
|
await processor?.updateTransformerOptions({ blurRadius })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying blur:', error)
|
||||||
|
} finally {
|
||||||
|
setProcessorPending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const videoElement = videoRef.current
|
||||||
|
if (!videoElement) return
|
||||||
|
|
||||||
|
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement)
|
||||||
|
attachVideoTrack()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!videoElement) return
|
||||||
|
localCameraTrack.detach(videoElement)
|
||||||
|
}
|
||||||
|
}, [localCameraTrack, isCameraEnabled])
|
||||||
|
|
||||||
|
const isSelected = (blurRadius: BlurRadius) => {
|
||||||
|
return isCameraEnabled && getBlurRadius() == blurRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipLabel = (blurRadius: BlurRadius) => {
|
||||||
|
return t(`blur.${isSelected(blurRadius) ? 'clear' : 'apply'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack padding="0 1.5rem" overflowY="scroll">
|
||||||
|
{localCameraTrack && isCameraEnabled ? (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
width="100%"
|
||||||
|
muted
|
||||||
|
style={{
|
||||||
|
transform: 'rotateY(180deg)',
|
||||||
|
minHeight: '175px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '174px',
|
||||||
|
display: 'flex',
|
||||||
|
backgroundColor: 'black',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<P
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
textAlign: 'center',
|
||||||
|
textWrap: 'balance',
|
||||||
|
marginBottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('activateCamera')}
|
||||||
|
</P>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Div
|
||||||
|
alignItems={'left'}
|
||||||
|
width={'100%'}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #dadce0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
margin: '0 .625rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<H
|
||||||
|
lvl={3}
|
||||||
|
style={{
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('heading')}
|
||||||
|
</H>
|
||||||
|
{ProcessorWrapper.isSupported ? (
|
||||||
|
<HStack>
|
||||||
|
<ToggleButton
|
||||||
|
size={'sm'}
|
||||||
|
legacyStyle
|
||||||
|
aria-label={tooltipLabel(BlurRadius.LIGHT)}
|
||||||
|
tooltip={tooltipLabel(BlurRadius.LIGHT)}
|
||||||
|
isDisabled={processorPending}
|
||||||
|
onPress={async () => await toggleBlur(BlurRadius.LIGHT)}
|
||||||
|
isSelected={isSelected(BlurRadius.LIGHT)}
|
||||||
|
>
|
||||||
|
{t('blur.light')}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
size={'sm'}
|
||||||
|
legacyStyle
|
||||||
|
aria-label={tooltipLabel(BlurRadius.NORMAL)}
|
||||||
|
tooltip={tooltipLabel(BlurRadius.NORMAL)}
|
||||||
|
isDisabled={processorPending}
|
||||||
|
onPress={async () => await toggleBlur(BlurRadius.NORMAL)}
|
||||||
|
isSelected={isSelected(BlurRadius.NORMAL)}
|
||||||
|
>
|
||||||
|
{t('blur.normal')}
|
||||||
|
</ToggleButton>
|
||||||
|
</HStack>
|
||||||
|
) : (
|
||||||
|
<Text variant="sm">{t('notAvailable')}</Text>
|
||||||
|
)}
|
||||||
|
<Information>
|
||||||
|
<Text
|
||||||
|
variant="sm"
|
||||||
|
style={{
|
||||||
|
textWrap: 'balance',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚠︎ {t('experimental')}
|
||||||
|
</Text>
|
||||||
|
</Information>
|
||||||
|
</Div>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { layoutStore } from '@/stores/layout'
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
import { Heading } from 'react-aria-components'
|
||||||
|
import { text } from '@/primitives/Text'
|
||||||
|
import { Box, Button, Div } from '@/primitives'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ParticipantsList } from './controls/Participants/ParticipantsList'
|
||||||
|
import { useSidePanel } from '../hooks/useSidePanel'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { Effects } from './Effects'
|
||||||
|
import { Chat } from '../prefabs/Chat'
|
||||||
|
|
||||||
|
type StyledSidePanelProps = {
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
onClose: () => void
|
||||||
|
isClosed: boolean
|
||||||
|
closeButtonTooltip: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledSidePanel = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
isClosed,
|
||||||
|
closeButtonTooltip,
|
||||||
|
}: StyledSidePanelProps) => (
|
||||||
|
<Box
|
||||||
|
size="sm"
|
||||||
|
className={css({
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: '1.5rem 1.5rem 1.5rem 0',
|
||||||
|
padding: 0,
|
||||||
|
gap: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: '80px',
|
||||||
|
width: '360px',
|
||||||
|
position: 'absolute',
|
||||||
|
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading
|
||||||
|
slot="title"
|
||||||
|
level={1}
|
||||||
|
className={text({ variant: 'h2' })}
|
||||||
|
style={{
|
||||||
|
paddingLeft: '1.5rem',
|
||||||
|
paddingTop: '1rem',
|
||||||
|
display: isClosed ? 'none' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
<Div
|
||||||
|
position="absolute"
|
||||||
|
top="5"
|
||||||
|
right="5"
|
||||||
|
style={{
|
||||||
|
display: isClosed ? 'none' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
invisible
|
||||||
|
size="xs"
|
||||||
|
onPress={onClose}
|
||||||
|
aria-label={closeButtonTooltip}
|
||||||
|
tooltip={closeButtonTooltip}
|
||||||
|
>
|
||||||
|
<RiCloseLine />
|
||||||
|
</Button>
|
||||||
|
</Div>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
type PanelProps = {
|
||||||
|
isOpen: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Panel = ({ isOpen, children }: PanelProps) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: isOpen ? 'inherit' : 'none',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SidePanel = () => {
|
||||||
|
const {
|
||||||
|
activePanelId,
|
||||||
|
isParticipantsOpen,
|
||||||
|
isEffectsOpen,
|
||||||
|
isChatOpen,
|
||||||
|
isSidePanelOpen,
|
||||||
|
} = useSidePanel()
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSidePanel
|
||||||
|
title={t(`heading.${activePanelId}`)}
|
||||||
|
onClose={() => (layoutStore.activePanelId = null)}
|
||||||
|
closeButtonTooltip={t('closeButton', {
|
||||||
|
content: t(`content.${activePanelId}`),
|
||||||
|
})}
|
||||||
|
isClosed={!isSidePanelOpen}
|
||||||
|
>
|
||||||
|
<Panel isOpen={isParticipantsOpen}>
|
||||||
|
<ParticipantsList />
|
||||||
|
</Panel>
|
||||||
|
<Panel isOpen={isEffectsOpen}>
|
||||||
|
<Effects />
|
||||||
|
</Panel>
|
||||||
|
<Panel isOpen={isChatOpen}>
|
||||||
|
<Chat />
|
||||||
|
</Panel>
|
||||||
|
</StyledSidePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { ReceivedChatMessage } from '@livekit/components-core'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
import { Text } from '@/primitives'
|
||||||
|
import { MessageFormatter } from '@livekit/components-react'
|
||||||
|
|
||||||
|
export interface ChatEntryProps extends React.HTMLAttributes<HTMLLIElement> {
|
||||||
|
entry: ReceivedChatMessage
|
||||||
|
hideMetadata?: boolean
|
||||||
|
messageFormatter?: MessageFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatEntry: (
|
||||||
|
props: ChatEntryProps & React.RefAttributes<HTMLLIElement>
|
||||||
|
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
ChatEntryProps
|
||||||
|
>(function ChatEntry(
|
||||||
|
{ entry, hideMetadata = false, messageFormatter, ...props }: ChatEntryProps,
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
// Fixme - Livekit messageFormatter strips '\n' char
|
||||||
|
const formattedMessage = React.useMemo(() => {
|
||||||
|
return messageFormatter ? messageFormatter(entry.message) : entry.message
|
||||||
|
}, [entry.message, messageFormatter])
|
||||||
|
const time = new Date(entry.timestamp)
|
||||||
|
const locale = navigator ? navigator.language : 'en-US'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.25rem',
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
title={time.toLocaleTimeString(locale, { timeStyle: 'full' })}
|
||||||
|
data-lk-message-origin={entry.from?.isLocal ? 'local' : 'remote'}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{!hideMetadata && (
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
paddingTop: '0.75rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text bold={true} variant="sm">
|
||||||
|
{entry.from?.name ?? entry.from?.identity}
|
||||||
|
</Text>
|
||||||
|
<Text variant="sm" className={css({ color: 'gray.700' })}>
|
||||||
|
{time.toLocaleTimeString(locale, { timeStyle: 'short' })}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
variant="sm"
|
||||||
|
margin={false}
|
||||||
|
className={css({
|
||||||
|
'& .lk-chat-link': {
|
||||||
|
color: 'blue',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formattedMessage}
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Button } from '@/primitives'
|
||||||
|
import { HStack } from '@/styled-system/jsx'
|
||||||
|
import { RiSendPlane2Fill } from '@remixicon/react'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { TextArea } from '@/primitives/TextArea'
|
||||||
|
import { RefObject } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const MAX_ROWS = 6
|
||||||
|
|
||||||
|
interface ChatInputProps {
|
||||||
|
inputRef: RefObject<HTMLTextAreaElement>
|
||||||
|
onSubmit: (text: string) => void
|
||||||
|
isSending: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatInput = ({
|
||||||
|
inputRef,
|
||||||
|
onSubmit,
|
||||||
|
isSending,
|
||||||
|
}: ChatInputProps) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat.input' })
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const [rows, setRows] = useState(1)
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit(text)
|
||||||
|
setText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = !text.trim() || isSending
|
||||||
|
|
||||||
|
const submitOnEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key !== 'Enter' || (e.key === 'Enter' && e.shiftKey)) return
|
||||||
|
e.preventDefault()
|
||||||
|
if (!isDisabled) handleSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resize = () => {
|
||||||
|
if (!inputRef.current) return
|
||||||
|
|
||||||
|
const textAreaLineHeight = 20 // Adjust this value based on your TextArea's line height
|
||||||
|
const previousRows = inputRef.current.rows
|
||||||
|
inputRef.current.rows = 1
|
||||||
|
|
||||||
|
const currentRows = Math.floor(
|
||||||
|
inputRef.current.scrollHeight / textAreaLineHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
if (currentRows === previousRows) {
|
||||||
|
inputRef.current.rows = currentRows
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRows >= MAX_ROWS) {
|
||||||
|
inputRef.current.rows = MAX_ROWS
|
||||||
|
inputRef.current.scrollTop = inputRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRows < MAX_ROWS) {
|
||||||
|
inputRef.current.style.overflowY = 'hidden'
|
||||||
|
} else {
|
||||||
|
inputRef.current.style.overflowY = 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
setRows(currentRows < MAX_ROWS ? currentRows : MAX_ROWS)
|
||||||
|
}
|
||||||
|
|
||||||
|
resize()
|
||||||
|
}, [text, inputRef])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack
|
||||||
|
style={{
|
||||||
|
margin: '0.75rem 0 1.5rem',
|
||||||
|
padding: '0.5rem',
|
||||||
|
backgroundColor: '#f3f4f6',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
ref={inputRef}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
submitOnEnter(e)
|
||||||
|
}}
|
||||||
|
onKeyUp={(e) => e.stopPropagation()}
|
||||||
|
placeholder={t('textArea.placeholder')}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => {
|
||||||
|
setText(e.target.value)
|
||||||
|
}}
|
||||||
|
rows={rows || 1}
|
||||||
|
style={{
|
||||||
|
border: 'none',
|
||||||
|
resize: 'none',
|
||||||
|
height: 'auto',
|
||||||
|
minHeight: `34px`,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
padding: '7px 10px',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
}}
|
||||||
|
placeholderStyle={'strong'}
|
||||||
|
spellCheck={false}
|
||||||
|
maxLength={500}
|
||||||
|
aria-label={t('textArea.label')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
square
|
||||||
|
invisible
|
||||||
|
size="sm"
|
||||||
|
onPress={handleSubmit}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
aria-label={t('button.label')}
|
||||||
|
>
|
||||||
|
<RiSendPlane2Fill />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiChat1Line } from '@remixicon/react'
|
import { RiChat1Line } from '@remixicon/react'
|
||||||
import { ToggleButton } from '@/primitives'
|
import { useSnapshot } from 'valtio'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
|
import { ToggleButton } from '@/primitives'
|
||||||
|
import { chatStore } from '@/stores/chat'
|
||||||
|
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||||
|
|
||||||
export const ChatToggle = () => {
|
export const ChatToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
|
||||||
|
|
||||||
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
|
const chatSnap = useSnapshot(chatStore)
|
||||||
|
|
||||||
|
const { isChatOpen, toggleChat } = useSidePanel()
|
||||||
const tooltipLabel = isChatOpen ? 'open' : 'closed'
|
const tooltipLabel = isChatOpen ? 'open' : 'closed'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,14 +24,15 @@ export const ChatToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.chat.${tooltipLabel}`)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(`controls.chat.${tooltipLabel}`)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isChatOpen}
|
isSelected={isChatOpen}
|
||||||
onPress={() => toggleChat()}
|
onPress={() => toggleChat()}
|
||||||
|
data-attr={`controls-chat-${tooltipLabel}`}
|
||||||
>
|
>
|
||||||
<RiChat1Line />
|
<RiChat1Line />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
{!!unreadMessages && (
|
{!!chatSnap.unreadMessages && (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -4,30 +4,16 @@ import { ToggleButton } from '@/primitives'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
|
||||||
|
|
||||||
export const HandToggle = () => {
|
export const HandToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||||
|
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||||
participant: room.localParticipant,
|
participant: room.localParticipant,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = isHandRaised
|
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||||
? t('controls.hand.lower')
|
|
||||||
: t('controls.hand.raise')
|
|
||||||
|
|
||||||
const notifyOtherParticipants = (isHandRaised: boolean) => {
|
|
||||||
room.localParticipant.publishData(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
!isHandRaised ? NotificationType.Raised : NotificationType.Lowered
|
|
||||||
),
|
|
||||||
{
|
|
||||||
reliable: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -39,13 +25,11 @@ export const HandToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={label}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={label}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isHandRaised}
|
isSelected={isHandRaised}
|
||||||
onPress={() => {
|
onPress={() => toggleRaisedHand()}
|
||||||
notifyOtherParticipants(isHandRaised)
|
data-attr={`controls-hand-${tooltipLabel}`}
|
||||||
toggleRaisedHand()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useConnectionState, useRoomContext } from '@livekit/components-react'
|
||||||
|
import { Button } from '@/primitives'
|
||||||
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
|
import { RiPhoneFill } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ConnectionState } from 'livekit-client'
|
||||||
|
|
||||||
|
export const LeaveButton = () => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
||||||
|
const room = useRoomContext()
|
||||||
|
const connectionState = useConnectionState(room)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isDisabled={connectionState === ConnectionState.Disconnected}
|
||||||
|
variant={'danger'}
|
||||||
|
tooltip={t('leave')}
|
||||||
|
aria-label={t('leave')}
|
||||||
|
onPress={() => {
|
||||||
|
room
|
||||||
|
.disconnect(true)
|
||||||
|
.catch((e) =>
|
||||||
|
console.error('An error occurred while disconnecting:', e)
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
navigateTo('feedback')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
data-attr="controls-leave"
|
||||||
|
>
|
||||||
|
<RiPhoneFill
|
||||||
|
style={{
|
||||||
|
transform: 'rotate(135deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+37
-29
@@ -1,13 +1,16 @@
|
|||||||
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
||||||
import {
|
import {
|
||||||
RiFeedbackLine,
|
RiAccountBoxLine,
|
||||||
RiQuestionLine,
|
RiMegaphoneLine,
|
||||||
RiSettings3Line,
|
RiSettings3Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
|
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dispatch, SetStateAction } from 'react'
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
|
import { DialogState } from './OptionsButton'
|
||||||
|
import { Separator } from '@/primitives/Separator'
|
||||||
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
|
import { RecordingMenuItem } from '@/features/rooms/livekit/components/controls/Options/RecordingMenuItem'
|
||||||
|
|
||||||
// @todo try refactoring it to use MenuList component
|
// @todo try refactoring it to use MenuList component
|
||||||
export const OptionsMenuItems = ({
|
export const OptionsMenuItems = ({
|
||||||
@@ -15,8 +18,8 @@ export const OptionsMenuItems = ({
|
|||||||
}: {
|
}: {
|
||||||
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
|
const { toggleEffects } = useSidePanel()
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
style={{
|
style={{
|
||||||
@@ -24,29 +27,34 @@ export const OptionsMenuItems = ({
|
|||||||
width: '300px',
|
width: '300px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<Section>
|
||||||
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
|
<MenuItem
|
||||||
target="_blank"
|
onAction={() => toggleEffects()}
|
||||||
className={menuItemRecipe({ icon: true })}
|
className={menuItemRecipe({ icon: true })}
|
||||||
>
|
>
|
||||||
<RiQuestionLine size={18} />
|
<RiAccountBoxLine size={20} />
|
||||||
{t('options.items.support')}
|
{t('effects')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<RecordingMenuItem />
|
||||||
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
</Section>
|
||||||
target="_blank"
|
<Separator />
|
||||||
className={menuItemRecipe({ icon: true })}
|
<Section>
|
||||||
>
|
<MenuItem
|
||||||
<RiFeedbackLine size={18} />
|
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||||
{t('options.items.feedbacks')}
|
target="_blank"
|
||||||
</MenuItem>
|
className={menuItemRecipe({ icon: true })}
|
||||||
<MenuItem
|
>
|
||||||
className={menuItemRecipe({ icon: true })}
|
<RiMegaphoneLine size={20} />
|
||||||
onAction={() => onOpenDialog('settings')}
|
{t('feedbacks')}
|
||||||
>
|
</MenuItem>
|
||||||
<RiSettings3Line size={18} />
|
<MenuItem
|
||||||
{t('options.items.settings')}
|
className={menuItemRecipe({ icon: true })}
|
||||||
</MenuItem>
|
onAction={() => onOpenDialog('settings')}
|
||||||
|
>
|
||||||
|
<RiSettings3Line size={20} />
|
||||||
|
{t('settings')}
|
||||||
|
</MenuItem>
|
||||||
|
</Section>
|
||||||
</RACMenu>
|
</RACMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import { MenuItem } from 'react-aria-components'
|
||||||
|
import { menuItemRecipe } from '@/primitives/menuItemRecipe.ts'
|
||||||
|
import { RiPauseCircleLine, RiRecordCircleLine } from '@remixicon/react'
|
||||||
|
import { useRecordRoom } from '@/features/rooms/livekit/api/recordRoom.ts'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { egressStore } from '@/stores/egress.ts'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
export const RecordingMenuItem = () => {
|
||||||
|
const { recordRoom, stopRecordingRoom } = useRecordRoom()
|
||||||
|
|
||||||
|
const egressSnap = useSnapshot(egressStore)
|
||||||
|
const egressId = egressSnap.egressId
|
||||||
|
|
||||||
|
const [isPending, setIsPending] = useState(false)
|
||||||
|
|
||||||
|
const handleAction = async () => {
|
||||||
|
if (egressId) {
|
||||||
|
setIsPending(true)
|
||||||
|
egressStore.egressIsStopping = true
|
||||||
|
const response = await stopRecordingRoom(egressId)
|
||||||
|
console.log(response)
|
||||||
|
egressStore.egressId = undefined
|
||||||
|
setIsPending(false)
|
||||||
|
} else {
|
||||||
|
setIsPending(true)
|
||||||
|
const response = await recordRoom()
|
||||||
|
egressStore.egressId = response['egress_id'] as string
|
||||||
|
setIsPending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
isDisabled={isPending}
|
||||||
|
className={menuItemRecipe({ icon: true })}
|
||||||
|
onAction={handleAction}
|
||||||
|
>
|
||||||
|
{egressId ? (
|
||||||
|
<>
|
||||||
|
<RiPauseCircleLine size={18} />
|
||||||
|
Arrêter l'enregistrement
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RiRecordCircleLine size={18} />
|
||||||
|
Enregistrer la réunion
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -73,6 +73,7 @@ export const HandRaisedListItem = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => lowerHandParticipant(participant)}
|
onPress={() => lowerHandParticipant(participant)}
|
||||||
tooltip={t('participants.lowerParticipantHand', { name })}
|
tooltip={t('participants.lowerParticipantHand', { name })}
|
||||||
|
data-attr="participants-lower-hand"
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ export const LowerAllHandsButton = ({
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="text"
|
variant="text"
|
||||||
onPress={() => lowerHandParticipants(participants)}
|
onPress={() => lowerHandParticipants(participants)}
|
||||||
|
data-attr="participants-lower-hands"
|
||||||
>
|
>
|
||||||
{t('participants.lowerParticipantsHand')}
|
{t('participants.lowerParticipantsHand')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
@@ -78,6 +78,7 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
|||||||
? muteParticipant(participant)
|
? muteParticipant(participant)
|
||||||
: setIsAlertOpen(true)
|
: setIsAlertOpen(true)
|
||||||
}
|
}
|
||||||
|
data-attr="participants-mute"
|
||||||
>
|
>
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<RiMicOffFill color={'gray'} />
|
<RiMicOffFill color={'gray'} />
|
||||||
|
|||||||
+40
-82
@@ -1,28 +1,21 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useParticipants } from '@livekit/components-react'
|
import { useParticipants } from '@livekit/components-react'
|
||||||
|
|
||||||
import { Heading } from 'react-aria-components'
|
import { Div, H } from '@/primitives'
|
||||||
import { Box, Button, Div, H } from '@/primitives'
|
|
||||||
import { text } from '@/primitives/Text'
|
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
|
||||||
import { participantsStore } from '@/stores/participants'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
|
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
|
||||||
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
|
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
|
||||||
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
|
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
|
||||||
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
|
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
|
||||||
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton'
|
|
||||||
|
|
||||||
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
||||||
export const ParticipantsList = () => {
|
export const ParticipantsList = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
|
||||||
|
|
||||||
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
|
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
|
||||||
// because the 'useLocalParticipant' hook does not update the participant's information when their
|
// because the 'useLocalParticipant' hook does not update the participant's information when their
|
||||||
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
|
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
|
||||||
const participants = useParticipants({
|
const participants = useParticipants()
|
||||||
updateOnlyOn: allParticipantRoomEvents,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedRemoteParticipants = participants
|
const sortedRemoteParticipants = participants
|
||||||
.slice(1)
|
.slice(1)
|
||||||
@@ -44,75 +37,40 @@ export const ParticipantsList = () => {
|
|||||||
|
|
||||||
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
|
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
|
||||||
return (
|
return (
|
||||||
<Box
|
<Div overflowY="scroll">
|
||||||
size="sm"
|
<H
|
||||||
minWidth="360px"
|
lvl={2}
|
||||||
className={css({
|
className={css({
|
||||||
overflow: 'hidden',
|
fontSize: '0.875rem',
|
||||||
display: 'flex',
|
fontWeight: 'bold',
|
||||||
flexDirection: 'column',
|
color: '#5f6368',
|
||||||
margin: '1.5rem 1.5rem 1.5rem 0',
|
padding: '0 1.5rem',
|
||||||
padding: 0,
|
marginBottom: '0.83em',
|
||||||
gap: 0,
|
})}
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
slot="title"
|
|
||||||
level={1}
|
|
||||||
className={text({ variant: 'h2' })}
|
|
||||||
style={{
|
|
||||||
paddingLeft: '1.5rem',
|
|
||||||
paddingTop: '1rem',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('participants.heading')}
|
{t('subheading').toUpperCase()}
|
||||||
</Heading>
|
</H>
|
||||||
<Div position="absolute" top="5" right="5">
|
{raisedHandParticipants.length > 0 && (
|
||||||
<Button
|
<Div marginBottom=".9375rem">
|
||||||
invisible
|
<ParticipantsCollapsableList
|
||||||
size="xs"
|
heading={t('raisedHands')}
|
||||||
onPress={() => (participantsStore.showParticipants = false)}
|
participants={raisedHandParticipants}
|
||||||
aria-label={t('participants.closeButton')}
|
renderParticipant={(participant) => (
|
||||||
tooltip={t('participants.closeButton')}
|
<HandRaisedListItem participant={participant} />
|
||||||
>
|
)}
|
||||||
<RiCloseLine />
|
action={() => (
|
||||||
</Button>
|
<LowerAllHandsButton participants={raisedHandParticipants} />
|
||||||
</Div>
|
)}
|
||||||
<Div overflowY="scroll">
|
/>
|
||||||
<H
|
</Div>
|
||||||
lvl={2}
|
)}
|
||||||
className={css({
|
<ParticipantsCollapsableList
|
||||||
fontSize: '0.875rem',
|
heading={t('contributors')}
|
||||||
fontWeight: 'bold',
|
participants={sortedParticipants}
|
||||||
color: '#5f6368',
|
renderParticipant={(participant) => (
|
||||||
padding: '0 1.5rem',
|
<ParticipantListItem participant={participant} />
|
||||||
marginBottom: '0.83em',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('participants.subheading').toUpperCase()}
|
|
||||||
</H>
|
|
||||||
{raisedHandParticipants.length > 0 && (
|
|
||||||
<Div marginBottom=".9375rem">
|
|
||||||
<ParticipantsCollapsableList
|
|
||||||
heading={t('participants.raisedHands')}
|
|
||||||
participants={raisedHandParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<HandRaisedListItem participant={participant} />
|
|
||||||
)}
|
|
||||||
action={() => (
|
|
||||||
<LowerAllHandsButton participants={raisedHandParticipants} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
)}
|
)}
|
||||||
<ParticipantsCollapsableList
|
/>
|
||||||
heading={t('participants.contributors')}
|
</Div>
|
||||||
participants={sortedParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<ParticipantListItem participant={participant} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -3,10 +3,10 @@ import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
|
|||||||
import { ToggleButton } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useParticipants } from '@livekit/components-react'
|
import { useParticipants } from '@livekit/components-react'
|
||||||
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
|
|
||||||
export const ParticipantsToggle = () => {
|
export const ParticipantsToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context could not be used due to inconsistent refresh behavior.
|
* Context could not be used due to inconsistent refresh behavior.
|
||||||
@@ -16,7 +16,7 @@ export const ParticipantsToggle = () => {
|
|||||||
const participants = useParticipants()
|
const participants = useParticipants()
|
||||||
const numParticipants = participants?.length
|
const numParticipants = participants?.length
|
||||||
|
|
||||||
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
|
const { isParticipantsOpen, toggleParticipants } = useSidePanel()
|
||||||
|
|
||||||
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
|
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
|
||||||
|
|
||||||
@@ -30,10 +30,11 @@ export const ParticipantsToggle = () => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.participants.${tooltipLabel}`)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(`controls.participants.${tooltipLabel}`)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isParticipantsOpen}
|
isSelected={isParticipantsOpen}
|
||||||
onPress={() => toggleParticipants()}
|
onPress={() => toggleParticipants()}
|
||||||
|
data-attr={`controls-participants-${tooltipLabel}`}
|
||||||
>
|
>
|
||||||
<RiGroupLine />
|
<RiGroupLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Div, ToggleButton } from '@/primitives'
|
||||||
|
import { RiArrowUpLine, RiCloseFill, RiRectangleLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const ScreenShareToggle = (
|
||||||
|
props: Omit<
|
||||||
|
UseTrackToggleProps<Track.Source.ScreenShare>,
|
||||||
|
'source' | 'captureOptions'
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
|
||||||
|
const { buttonProps, enabled } = useTrackToggle({
|
||||||
|
...props,
|
||||||
|
source: Track.Source.ScreenShare,
|
||||||
|
captureOptions: { audio: true, selfBrowserSurface: 'include' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipLabel = enabled ? 'stop' : 'start'
|
||||||
|
const Icon = enabled ? RiCloseFill : RiArrowUpLine
|
||||||
|
|
||||||
|
// fixme - remove ToggleButton custom styles when we design a proper icon
|
||||||
|
return (
|
||||||
|
<ToggleButton
|
||||||
|
isSelected={enabled}
|
||||||
|
square
|
||||||
|
legacyStyle
|
||||||
|
tooltip={t(tooltipLabel)}
|
||||||
|
onPress={(e) =>
|
||||||
|
buttonProps.onClick?.(
|
||||||
|
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
maxWidth: '46px',
|
||||||
|
maxHeight: '46px',
|
||||||
|
}}
|
||||||
|
data-attr={`controls-screenshare-${tooltipLabel}`}
|
||||||
|
>
|
||||||
|
<Div position="relative">
|
||||||
|
<RiRectangleLine size={28} />
|
||||||
|
<Icon
|
||||||
|
size={16}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Div>
|
||||||
|
</ToggleButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
useMediaDeviceSelect,
|
||||||
|
useTrackToggle,
|
||||||
|
UseTrackToggleProps,
|
||||||
|
} from '@livekit/components-react'
|
||||||
|
import { HStack } from '@/styled-system/jsx'
|
||||||
|
import { Button, Menu, MenuList } from '@/primitives'
|
||||||
|
import {
|
||||||
|
RemixiconComponentType,
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiMicLine,
|
||||||
|
RiMicOffLine,
|
||||||
|
RiVideoOffLine,
|
||||||
|
RiVideoOnLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { Track } from 'livekit-client'
|
||||||
|
|
||||||
|
import { Shortcut } from '@/features/shortcuts/types'
|
||||||
|
|
||||||
|
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||||
|
|
||||||
|
export type ToggleSource = Exclude<
|
||||||
|
Track.Source,
|
||||||
|
Track.Source.ScreenShareAudio | Track.Source.Unknown
|
||||||
|
>
|
||||||
|
|
||||||
|
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
|
||||||
|
|
||||||
|
export type SelectToggleDeviceConfig = {
|
||||||
|
kind: MediaDeviceKind
|
||||||
|
iconOn: RemixiconComponentType
|
||||||
|
iconOff: RemixiconComponentType
|
||||||
|
shortcut?: Shortcut
|
||||||
|
longPress?: Shortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectToggleDeviceConfigMap = {
|
||||||
|
[key in SelectToggleSource]: SelectToggleDeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
|
||||||
|
[Track.Source.Microphone]: {
|
||||||
|
kind: 'audioinput',
|
||||||
|
iconOn: RiMicLine,
|
||||||
|
iconOff: RiMicOffLine,
|
||||||
|
shortcut: {
|
||||||
|
key: 'd',
|
||||||
|
ctrlKey: true,
|
||||||
|
},
|
||||||
|
longPress: {
|
||||||
|
key: 'Space',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Track.Source.Camera]: {
|
||||||
|
kind: 'videoinput',
|
||||||
|
iconOn: RiVideoOnLine,
|
||||||
|
iconOff: RiVideoOffLine,
|
||||||
|
shortcut: {
|
||||||
|
key: 'e',
|
||||||
|
ctrlKey: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||||
|
UseTrackToggleProps<T> & {
|
||||||
|
onActiveDeviceChange: (deviceId: string) => void
|
||||||
|
source: SelectToggleSource
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectToggleDevice = <T extends ToggleSource>({
|
||||||
|
onActiveDeviceChange,
|
||||||
|
...props
|
||||||
|
}: SelectToggleDeviceProps<T>) => {
|
||||||
|
const config = selectToggleDeviceConfig[props.source]
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Invalid source')
|
||||||
|
}
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
const trackProps = useTrackToggle(props)
|
||||||
|
|
||||||
|
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||||
|
useMediaDeviceSelect({ kind: config.kind })
|
||||||
|
|
||||||
|
const selectLabel = t('choose', { keyPrefix: `join.${config.kind}` })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack gap={0}>
|
||||||
|
<ToggleDevice {...trackProps} config={config} />
|
||||||
|
<Menu>
|
||||||
|
<Button
|
||||||
|
tooltip={selectLabel}
|
||||||
|
aria-label={selectLabel}
|
||||||
|
groupPosition="right"
|
||||||
|
square
|
||||||
|
>
|
||||||
|
<RiArrowDownSLine />
|
||||||
|
</Button>
|
||||||
|
<MenuList
|
||||||
|
items={devices.map((d) => ({
|
||||||
|
value: d.deviceId,
|
||||||
|
label: d.label,
|
||||||
|
}))}
|
||||||
|
selectedItem={activeDeviceId}
|
||||||
|
onAction={(value) => {
|
||||||
|
setActiveMediaDevice(value as string)
|
||||||
|
onActiveDeviceChange(value as string)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { ToggleButton } from '@/primitives'
|
||||||
|
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { SelectToggleDeviceConfig } from './SelectToggleDevice'
|
||||||
|
import useLongPress from '@/features/shortcuts/useLongPress'
|
||||||
|
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||||
|
import { useIsSpeaking, useLocalParticipant } from '@livekit/components-react'
|
||||||
|
|
||||||
|
export type ToggleDeviceProps = {
|
||||||
|
enabled: boolean
|
||||||
|
toggle: () => void
|
||||||
|
config: SelectToggleDeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleDevice = ({
|
||||||
|
config,
|
||||||
|
enabled,
|
||||||
|
toggle,
|
||||||
|
}: ToggleDeviceProps) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
|
const { kind, shortcut, iconOn, iconOff, longPress } = config
|
||||||
|
|
||||||
|
const [pushToTalk, setPushToTalk] = useState(false)
|
||||||
|
|
||||||
|
const onKeyDown = () => {
|
||||||
|
if (pushToTalk || enabled) return
|
||||||
|
toggle()
|
||||||
|
setPushToTalk(true)
|
||||||
|
}
|
||||||
|
const onKeyUp = () => {
|
||||||
|
if (!pushToTalk) return
|
||||||
|
toggle()
|
||||||
|
setPushToTalk(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
|
||||||
|
useLongPress({ keyCode: longPress?.key, onKeyDown, onKeyUp })
|
||||||
|
|
||||||
|
const toggleLabel = useMemo(() => {
|
||||||
|
const label = t(enabled ? 'disable' : 'enable', {
|
||||||
|
keyPrefix: `join.${kind}`,
|
||||||
|
})
|
||||||
|
return shortcut ? appendShortcutLabel(label, shortcut) : label
|
||||||
|
}, [enabled, kind, shortcut, t])
|
||||||
|
|
||||||
|
const Icon = enabled ? iconOn : iconOff
|
||||||
|
|
||||||
|
const { localParticipant } = useLocalParticipant()
|
||||||
|
const isSpeaking = useIsSpeaking(localParticipant)
|
||||||
|
|
||||||
|
if (kind === 'audioinput' && pushToTalk) {
|
||||||
|
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButton
|
||||||
|
isSelected={enabled}
|
||||||
|
variant={enabled ? undefined : 'danger'}
|
||||||
|
toggledStyles={false}
|
||||||
|
onPress={() => toggle()}
|
||||||
|
aria-label={toggleLabel}
|
||||||
|
tooltip={toggleLabel}
|
||||||
|
groupPosition="left"
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</ToggleButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { RoomEvent } from 'livekit-client'
|
|
||||||
|
|
||||||
// Issue: 'allRemoteParticipantRoomEvents' is not exposed or importable. One event is missing
|
|
||||||
// to trigger the real-time update of participants when they change their name.
|
|
||||||
// This code is duplicated from LiveKit.
|
|
||||||
export const allRemoteParticipantRoomEvents = [
|
|
||||||
RoomEvent.ConnectionStateChanged,
|
|
||||||
RoomEvent.RoomMetadataChanged,
|
|
||||||
|
|
||||||
RoomEvent.ActiveSpeakersChanged,
|
|
||||||
RoomEvent.ConnectionQualityChanged,
|
|
||||||
|
|
||||||
RoomEvent.ParticipantConnected,
|
|
||||||
RoomEvent.ParticipantDisconnected,
|
|
||||||
RoomEvent.ParticipantPermissionsChanged,
|
|
||||||
RoomEvent.ParticipantMetadataChanged,
|
|
||||||
RoomEvent.ParticipantNameChanged, // This element is missing in LiveKit and causes problems
|
|
||||||
|
|
||||||
RoomEvent.TrackMuted,
|
|
||||||
RoomEvent.TrackUnmuted,
|
|
||||||
RoomEvent.TrackPublished,
|
|
||||||
RoomEvent.TrackUnpublished,
|
|
||||||
RoomEvent.TrackStreamStateChanged,
|
|
||||||
RoomEvent.TrackSubscriptionFailed,
|
|
||||||
RoomEvent.TrackSubscriptionPermissionChanged,
|
|
||||||
RoomEvent.TrackSubscriptionStatusChanged,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const allParticipantRoomEvents = [
|
|
||||||
...allRemoteParticipantRoomEvents,
|
|
||||||
RoomEvent.LocalTrackPublished,
|
|
||||||
RoomEvent.LocalTrackUnpublished,
|
|
||||||
]
|
|
||||||
+1
@@ -7,6 +7,7 @@ type useRaisedHandProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRaisedHand({ participant }: useRaisedHandProps) {
|
export function useRaisedHand({ participant }: useRaisedHandProps) {
|
||||||
|
// fixme - refactor this part to rely on attributes
|
||||||
const { metadata } = useParticipantInfo({ participant })
|
const { metadata } = useParticipantInfo({ participant })
|
||||||
const parsedMetadata = JSON.parse(metadata || '{}')
|
const parsedMetadata = JSON.parse(metadata || '{}')
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { layoutStore } from '@/stores/layout'
|
||||||
|
|
||||||
|
export enum PanelId {
|
||||||
|
PARTICIPANTS = 'participants',
|
||||||
|
EFFECTS = 'effects',
|
||||||
|
CHAT = 'chat',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSidePanel = () => {
|
||||||
|
const layoutSnap = useSnapshot(layoutStore)
|
||||||
|
const activePanelId = layoutSnap.activePanelId
|
||||||
|
|
||||||
|
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
|
||||||
|
const isEffectsOpen = activePanelId == PanelId.EFFECTS
|
||||||
|
const isChatOpen = activePanelId == PanelId.CHAT
|
||||||
|
const isSidePanelOpen = !!activePanelId
|
||||||
|
|
||||||
|
const toggleParticipants = () => {
|
||||||
|
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleChat = () => {
|
||||||
|
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleEffects = () => {
|
||||||
|
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activePanelId,
|
||||||
|
toggleParticipants,
|
||||||
|
toggleChat,
|
||||||
|
toggleEffects,
|
||||||
|
isChatOpen,
|
||||||
|
isParticipantsOpen,
|
||||||
|
isEffectsOpen,
|
||||||
|
isSidePanelOpen,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useLayoutContext } from '@livekit/components-react'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { participantsStore } from '@/stores/participants.ts'
|
|
||||||
|
|
||||||
export const useWidgetInteraction = () => {
|
|
||||||
const { dispatch, state } = useLayoutContext().widget
|
|
||||||
|
|
||||||
const participantsSnap = useSnapshot(participantsStore)
|
|
||||||
const isParticipantsOpen = participantsSnap.showParticipants
|
|
||||||
|
|
||||||
const toggleParticipants = () => {
|
|
||||||
if (dispatch && state?.showChat) {
|
|
||||||
dispatch({ msg: 'toggle_chat' })
|
|
||||||
}
|
|
||||||
participantsStore.showParticipants = !isParticipantsOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleChat = () => {
|
|
||||||
if (isParticipantsOpen) {
|
|
||||||
participantsStore.showParticipants = false
|
|
||||||
}
|
|
||||||
if (dispatch) {
|
|
||||||
dispatch({ msg: 'toggle_chat' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toggleParticipants,
|
|
||||||
toggleChat,
|
|
||||||
isChatOpen: state?.showChat,
|
|
||||||
unreadMessages: state?.unreadMessages,
|
|
||||||
isParticipantsOpen,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import type { ChatMessage, ChatOptions } from '@livekit/components-core'
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
formatChatMessageLinks,
|
||||||
|
useChat,
|
||||||
|
useParticipants,
|
||||||
|
} from '@livekit/components-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { chatStore } from '@/stores/chat'
|
||||||
|
import { Div, Text } from '@/primitives'
|
||||||
|
import { ChatInput } from '../components/chat/Input'
|
||||||
|
import { ChatEntry } from '../components/chat/Entry'
|
||||||
|
import { useSidePanel } from '../hooks/useSidePanel'
|
||||||
|
|
||||||
|
export interface ChatProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
ChatOptions {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Chat component adds a basis chat functionality to the LiveKit room. The messages are distributed to all participants
|
||||||
|
* in the room. Only users who are in the room at the time of dispatch will receive the message.
|
||||||
|
*/
|
||||||
|
export function Chat({ ...props }: ChatProps) {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'chat' })
|
||||||
|
|
||||||
|
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
const ulRef = React.useRef<HTMLUListElement>(null)
|
||||||
|
|
||||||
|
const { send, chatMessages, isSending } = useChat()
|
||||||
|
|
||||||
|
const { isChatOpen } = useSidePanel()
|
||||||
|
const chatSnap = useSnapshot(chatStore)
|
||||||
|
|
||||||
|
// Use useParticipants hook to trigger a re-render when the participant list changes.
|
||||||
|
const participants = useParticipants()
|
||||||
|
|
||||||
|
const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(0)
|
||||||
|
|
||||||
|
async function handleSubmit(text: string) {
|
||||||
|
if (!send || !text) return
|
||||||
|
await send(text)
|
||||||
|
inputRef?.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (chatMessages.length > 0 && ulRef.current) {
|
||||||
|
ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight })
|
||||||
|
}
|
||||||
|
}, [ulRef, chatMessages])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (chatMessages.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isChatOpen &&
|
||||||
|
lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp
|
||||||
|
) {
|
||||||
|
lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp
|
||||||
|
chatStore.unreadMessages = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadMessageCount = chatMessages.filter(
|
||||||
|
(msg) => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current
|
||||||
|
).length
|
||||||
|
|
||||||
|
if (
|
||||||
|
unreadMessageCount > 0 &&
|
||||||
|
chatSnap.unreadMessages !== unreadMessageCount
|
||||||
|
) {
|
||||||
|
chatStore.unreadMessages = unreadMessageCount
|
||||||
|
}
|
||||||
|
}, [chatMessages, chatSnap.unreadMessages, isChatOpen])
|
||||||
|
|
||||||
|
const renderedMessages = React.useMemo(() => {
|
||||||
|
return chatMessages.map((msg, idx, allMsg) => {
|
||||||
|
const hideMetadata =
|
||||||
|
idx >= 1 &&
|
||||||
|
msg.timestamp - allMsg[idx - 1].timestamp < 60_000 &&
|
||||||
|
allMsg[idx - 1].from === msg.from
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatEntry
|
||||||
|
key={msg.id ?? idx}
|
||||||
|
hideMetadata={hideMetadata}
|
||||||
|
entry={msg}
|
||||||
|
messageFormatter={formatChatMessageLinks}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// This ensures that the chat message list is updated to reflect any changes in participant information.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [chatMessages, participants])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Div
|
||||||
|
display={'flex'}
|
||||||
|
padding={'0 1.5rem'}
|
||||||
|
flexGrow={1}
|
||||||
|
flexDirection={'column'}
|
||||||
|
minHeight={0}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
variant="sm"
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: '#f3f4f6',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('disclaimer')}
|
||||||
|
</Text>
|
||||||
|
<Div
|
||||||
|
flexGrow={1}
|
||||||
|
flexDirection={'column'}
|
||||||
|
minHeight={0}
|
||||||
|
overflowY="scroll"
|
||||||
|
>
|
||||||
|
<ul className="lk-list lk-chat-messages" ref={ulRef}>
|
||||||
|
{renderedMessages}
|
||||||
|
</ul>
|
||||||
|
</Div>
|
||||||
|
<ChatInput
|
||||||
|
inputRef={inputRef}
|
||||||
|
onSubmit={(e) => handleSubmit(e)}
|
||||||
|
isSending={isSending}
|
||||||
|
/>
|
||||||
|
</Div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,22 +4,20 @@ import * as React from 'react'
|
|||||||
import { supportsScreenSharing } from '@livekit/components-core'
|
import { supportsScreenSharing } from '@livekit/components-core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DisconnectButton,
|
|
||||||
LeaveIcon,
|
|
||||||
MediaDeviceMenu,
|
|
||||||
TrackToggle,
|
|
||||||
useMaybeLayoutContext,
|
useMaybeLayoutContext,
|
||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
|
|
||||||
import { mergeProps } from '@/utils/mergeProps.ts'
|
|
||||||
import { StartMediaButton } from '../components/controls/StartMediaButton'
|
import { StartMediaButton } from '../components/controls/StartMediaButton'
|
||||||
import { useMediaQuery } from '../hooks/useMediaQuery'
|
import { useMediaQuery } from '../hooks/useMediaQuery'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
||||||
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
|
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
|
||||||
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
|
import { ChatToggle } from '../components/controls/ChatToggle'
|
||||||
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
|
import { HandToggle } from '../components/controls/HandToggle'
|
||||||
|
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
|
||||||
|
import { LeaveButton } from '../components/controls/LeaveButton'
|
||||||
|
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type ControlBarControls = {
|
export type ControlBarControls = {
|
||||||
@@ -38,7 +36,7 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
controls?: ControlBarControls
|
controls?: ControlBarControls
|
||||||
/**
|
/**
|
||||||
* If `true`, the user's device choices will be persisted.
|
* If `true`, the user's device choices will be persisted.
|
||||||
* This will enables the user to have the same device choices when they rejoin the room.
|
* This will enable the user to have the same device choices when they rejoin the room.
|
||||||
* @defaultValue true
|
* @defaultValue true
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
@@ -65,9 +63,7 @@ export function ControlBar({
|
|||||||
variation,
|
variation,
|
||||||
saveUserChoices = true,
|
saveUserChoices = true,
|
||||||
onDeviceError,
|
onDeviceError,
|
||||||
...props
|
|
||||||
}: ControlBarProps) {
|
}: ControlBarProps) {
|
||||||
const { t } = useTranslation('rooms')
|
|
||||||
const [isChatOpen, setIsChatOpen] = React.useState(false)
|
const [isChatOpen, setIsChatOpen] = React.useState(false)
|
||||||
const layoutContext = useMaybeLayoutContext()
|
const layoutContext = useMaybeLayoutContext()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -83,28 +79,8 @@ export function ControlBar({
|
|||||||
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
|
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
|
||||||
variation ??= defaultVariation
|
variation ??= defaultVariation
|
||||||
|
|
||||||
const showIcon = React.useMemo(
|
|
||||||
() => variation === 'minimal' || variation === 'verbose',
|
|
||||||
[variation]
|
|
||||||
)
|
|
||||||
const showText = React.useMemo(
|
|
||||||
() => variation === 'textOnly' || variation === 'verbose',
|
|
||||||
[variation]
|
|
||||||
)
|
|
||||||
|
|
||||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||||
|
|
||||||
const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false)
|
|
||||||
|
|
||||||
const onScreenShareChange = React.useCallback(
|
|
||||||
(enabled: boolean) => {
|
|
||||||
setIsScreenShareEnabled(enabled)
|
|
||||||
},
|
|
||||||
[setIsScreenShareEnabled]
|
|
||||||
)
|
|
||||||
|
|
||||||
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
saveAudioInputEnabled,
|
saveAudioInputEnabled,
|
||||||
saveVideoInputEnabled,
|
saveVideoInputEnabled,
|
||||||
@@ -125,73 +101,55 @@ export function ControlBar({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...htmlProps}>
|
<div
|
||||||
<div className="lk-button-group">
|
className={css({
|
||||||
<TrackToggle
|
display: 'flex',
|
||||||
source={Track.Source.Microphone}
|
gap: '.5rem',
|
||||||
showIcon={showIcon}
|
alignItems: 'center',
|
||||||
onChange={microphoneOnChange}
|
justifyContent: 'center',
|
||||||
onDeviceError={(error) =>
|
padding: '.75rem',
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
borderTop: '1px solid var(--lk-border-color)',
|
||||||
}
|
maxHeight: 'var(--lk-control-bar-height)',
|
||||||
>
|
height: '80px',
|
||||||
{showText && t('controls.microphone')}
|
position: 'absolute',
|
||||||
</TrackToggle>
|
backgroundColor: '#d1d5db',
|
||||||
<div className="lk-button-group-menu">
|
bottom: 0,
|
||||||
<MediaDeviceMenu
|
left: 0,
|
||||||
kind="audioinput"
|
right: 0,
|
||||||
onActiveDeviceChange={(_kind, deviceId) =>
|
})}
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
>
|
||||||
}
|
<SelectToggleDevice
|
||||||
/>
|
source={Track.Source.Microphone}
|
||||||
</div>
|
onChange={microphoneOnChange}
|
||||||
</div>
|
onDeviceError={(error) =>
|
||||||
<div className="lk-button-group">
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
<TrackToggle
|
}
|
||||||
source={Track.Source.Camera}
|
onActiveDeviceChange={(deviceId) =>
|
||||||
showIcon={showIcon}
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
onChange={cameraOnChange}
|
}
|
||||||
onDeviceError={(error) =>
|
/>
|
||||||
onDeviceError?.({ source: Track.Source.Camera, error })
|
<SelectToggleDevice
|
||||||
}
|
source={Track.Source.Camera}
|
||||||
>
|
onChange={cameraOnChange}
|
||||||
{showText && t('controls.camera')}
|
onDeviceError={(error) =>
|
||||||
</TrackToggle>
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
<div className="lk-button-group-menu">
|
}
|
||||||
<MediaDeviceMenu
|
onActiveDeviceChange={(deviceId) =>
|
||||||
kind="videoinput"
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
onActiveDeviceChange={(_kind, deviceId) =>
|
}
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{browserSupportsScreenSharing && (
|
{browserSupportsScreenSharing && (
|
||||||
<TrackToggle
|
<ScreenShareToggle
|
||||||
source={Track.Source.ScreenShare}
|
|
||||||
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
|
|
||||||
showIcon={showIcon}
|
|
||||||
onChange={onScreenShareChange}
|
|
||||||
onDeviceError={(error) =>
|
onDeviceError={(error) =>
|
||||||
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
{showText &&
|
|
||||||
t(
|
|
||||||
isScreenShareEnabled
|
|
||||||
? 'controls.stopScreenShare'
|
|
||||||
: 'controls.shareScreen'
|
|
||||||
)}
|
|
||||||
</TrackToggle>
|
|
||||||
)}
|
)}
|
||||||
<HandToggle />
|
<HandToggle />
|
||||||
<ChatToggle />
|
<ChatToggle />
|
||||||
<ParticipantsToggle />
|
<ParticipantsToggle />
|
||||||
<OptionsButton />
|
<OptionsButton />
|
||||||
<DisconnectButton>
|
<LeaveButton />
|
||||||
{showIcon && <LeaveIcon />}
|
|
||||||
{showText && t('controls.leave')}
|
|
||||||
</DisconnectButton>
|
|
||||||
<StartMediaButton />
|
<StartMediaButton />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type {
|
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
MessageDecoder,
|
|
||||||
MessageEncoder,
|
|
||||||
TrackReferenceOrPlaceholder,
|
|
||||||
WidgetState,
|
|
||||||
} from '@livekit/components-core'
|
|
||||||
import {
|
import {
|
||||||
isEqualTrackRef,
|
isEqualTrackRef,
|
||||||
isTrackReference,
|
isTrackReference,
|
||||||
@@ -20,22 +15,20 @@ import {
|
|||||||
GridLayout,
|
GridLayout,
|
||||||
LayoutContextProvider,
|
LayoutContextProvider,
|
||||||
RoomAudioRenderer,
|
RoomAudioRenderer,
|
||||||
MessageFormatter,
|
|
||||||
usePinnedTracks,
|
usePinnedTracks,
|
||||||
useTracks,
|
useTracks,
|
||||||
useCreateLayoutContext,
|
useCreateLayoutContext,
|
||||||
Chat,
|
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
|
|
||||||
import { ControlBar } from './ControlBar'
|
import { ControlBar } from './ControlBar'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { cva } from '@/styled-system/css'
|
import { cva } from '@/styled-system/css'
|
||||||
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
|
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { participantsStore } from '@/stores/participants'
|
|
||||||
import { FocusLayout } from '../components/FocusLayout'
|
import { FocusLayout } from '../components/FocusLayout'
|
||||||
import { ParticipantTile } from '../components/ParticipantTile'
|
import { ParticipantTile } from '../components/ParticipantTile'
|
||||||
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
import { SidePanel } from '../components/SidePanel'
|
||||||
|
import { useSidePanel } from '../hooks/useSidePanel'
|
||||||
|
import { RecordingIndicator } from '@/features/rooms/components/RecordingIndicator.tsx'
|
||||||
|
|
||||||
const LayoutWrapper = styled(
|
const LayoutWrapper = styled(
|
||||||
'div',
|
'div',
|
||||||
@@ -44,7 +37,7 @@ const LayoutWrapper = styled(
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 'calc(100% - var(--lk-control-bar-height))',
|
height: '100%',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -54,9 +47,6 @@ const LayoutWrapper = styled(
|
|||||||
*/
|
*/
|
||||||
export interface VideoConferenceProps
|
export interface VideoConferenceProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement> {
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
chatMessageFormatter?: MessageFormatter
|
|
||||||
chatMessageEncoder?: MessageEncoder
|
|
||||||
chatMessageDecoder?: MessageDecoder
|
|
||||||
/** @alpha */
|
/** @alpha */
|
||||||
SettingsComponent?: React.ComponentType
|
SettingsComponent?: React.ComponentType
|
||||||
}
|
}
|
||||||
@@ -79,17 +69,7 @@ export interface VideoConferenceProps
|
|||||||
* ```
|
* ```
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function VideoConference({
|
export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||||
chatMessageFormatter,
|
|
||||||
chatMessageDecoder,
|
|
||||||
chatMessageEncoder,
|
|
||||||
...props
|
|
||||||
}: VideoConferenceProps) {
|
|
||||||
const [widgetState, setWidgetState] = React.useState<WidgetState>({
|
|
||||||
showChat: false,
|
|
||||||
unreadMessages: 0,
|
|
||||||
showSettings: false,
|
|
||||||
})
|
|
||||||
const lastAutoFocusedScreenShareTrack =
|
const lastAutoFocusedScreenShareTrack =
|
||||||
React.useRef<TrackReferenceOrPlaceholder | null>(null)
|
React.useRef<TrackReferenceOrPlaceholder | null>(null)
|
||||||
|
|
||||||
@@ -101,11 +81,6 @@ export function VideoConference({
|
|||||||
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false }
|
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const widgetUpdate = (state: WidgetState) => {
|
|
||||||
log.debug('updating widget state', state)
|
|
||||||
setWidgetState(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutContext = useCreateLayoutContext()
|
const layoutContext = useCreateLayoutContext()
|
||||||
|
|
||||||
const screenShareTracks = tracks
|
const screenShareTracks = tracks
|
||||||
@@ -172,8 +147,7 @@ export function VideoConference({
|
|||||||
])
|
])
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
const participantsSnap = useSnapshot(participantsStore)
|
const { isSidePanelOpen } = useSidePanel()
|
||||||
const showParticipants = participantsSnap.showParticipants
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lk-video-conference" {...props}>
|
<div className="lk-video-conference" {...props}>
|
||||||
@@ -181,9 +155,18 @@ export function VideoConference({
|
|||||||
<LayoutContextProvider
|
<LayoutContextProvider
|
||||||
value={layoutContext}
|
value={layoutContext}
|
||||||
// onPinChange={handleFocusStateChange}
|
// onPinChange={handleFocusStateChange}
|
||||||
onWidgetChange={widgetUpdate}
|
|
||||||
>
|
>
|
||||||
<div className="lk-video-conference-inner">
|
<div
|
||||||
|
// todo - extract these magic values into constant
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: isSidePanelOpen
|
||||||
|
? 'var(--lk-grid-gap) calc(358px + 3rem) calc(80px + var(--lk-grid-gap)) 16px'
|
||||||
|
: 'var(--lk-grid-gap) var(--lk-grid-gap) calc(80px + var(--lk-grid-gap))',
|
||||||
|
transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecordingIndicator />
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<div
|
<div
|
||||||
style={{ display: 'flex', position: 'relative', width: '100%' }}
|
style={{ display: 'flex', position: 'relative', width: '100%' }}
|
||||||
@@ -193,7 +176,7 @@ export function VideoConference({
|
|||||||
className="lk-grid-layout-wrapper"
|
className="lk-grid-layout-wrapper"
|
||||||
style={{ height: 'auto' }}
|
style={{ height: 'auto' }}
|
||||||
>
|
>
|
||||||
<GridLayout tracks={tracks}>
|
<GridLayout tracks={tracks} style={{ padding: 0 }}>
|
||||||
<ParticipantTile />
|
<ParticipantTile />
|
||||||
</GridLayout>
|
</GridLayout>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +185,7 @@ export function VideoConference({
|
|||||||
className="lk-focus-layout-wrapper"
|
className="lk-focus-layout-wrapper"
|
||||||
style={{ height: 'auto' }}
|
style={{ height: 'auto' }}
|
||||||
>
|
>
|
||||||
<FocusLayoutContainer>
|
<FocusLayoutContainer style={{ padding: 0 }}>
|
||||||
<CarouselLayout
|
<CarouselLayout
|
||||||
tracks={carouselTracks}
|
tracks={carouselTracks}
|
||||||
style={{
|
style={{
|
||||||
@@ -215,18 +198,12 @@ export function VideoConference({
|
|||||||
</FocusLayoutContainer>
|
</FocusLayoutContainer>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MainNotificationToast />
|
|
||||||
</div>
|
</div>
|
||||||
<Chat
|
|
||||||
style={{ display: widgetState.showChat ? 'grid' : 'none' }}
|
|
||||||
messageFormatter={chatMessageFormatter}
|
|
||||||
messageEncoder={chatMessageEncoder}
|
|
||||||
messageDecoder={chatMessageDecoder}
|
|
||||||
/>
|
|
||||||
{showParticipants && <ParticipantsList />}
|
|
||||||
</LayoutWrapper>
|
</LayoutWrapper>
|
||||||
<ControlBar />
|
<MainNotificationToast />
|
||||||
</div>
|
</div>
|
||||||
|
<ControlBar />
|
||||||
|
<SidePanel />
|
||||||
</LayoutContextProvider>
|
</LayoutContextProvider>
|
||||||
)}
|
)}
|
||||||
<RoomAudioRenderer />
|
<RoomAudioRenderer />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ErrorScreen } from '@/components/ErrorScreen'
|
|||||||
import { useUser, UserAware } from '@/features/auth'
|
import { useUser, UserAware } from '@/features/auth'
|
||||||
import { Conference } from '../components/Conference'
|
import { Conference } from '../components/Conference'
|
||||||
import { Join } from '../components/Join'
|
import { Join } from '../components/Join'
|
||||||
|
import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts'
|
||||||
|
|
||||||
export const Room = () => {
|
export const Room = () => {
|
||||||
const { isLoggedIn } = useUser()
|
const { isLoggedIn } = useUser()
|
||||||
@@ -19,6 +20,8 @@ export const Room = () => {
|
|||||||
const mode = isLoggedIn && history.state?.create ? 'create' : 'join'
|
const mode = isLoggedIn && history.state?.create ? 'create' : 'join'
|
||||||
const skipJoinScreen = isLoggedIn && mode === 'create'
|
const skipJoinScreen = isLoggedIn && mode === 'create'
|
||||||
|
|
||||||
|
useKeyboardShortcuts()
|
||||||
|
|
||||||
const clearRouterState = () => {
|
const clearRouterState = () => {
|
||||||
if (window?.history?.state) {
|
if (window?.history?.state) {
|
||||||
window.history.replaceState({}, '')
|
window.history.replaceState({}, '')
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
|
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
|
||||||
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
|
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
|
|
||||||
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
|
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
|
||||||
|
|
||||||
@@ -28,9 +29,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<P>{t('account.youAreNotLoggedIn')}</P>
|
<P>{t('account.youAreNotLoggedIn')}</P>
|
||||||
<P>
|
<ProConnectButton />
|
||||||
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
|
|
||||||
</P>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<H lvl={2}>{t('language.heading')}</H>
|
<H lvl={2}>{t('language.heading')}</H>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
useRoomContext,
|
useRoomContext,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
|
|
||||||
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
Pick<TabPanelProps, 'id'>
|
Pick<TabPanelProps, 'id'>
|
||||||
@@ -58,9 +59,7 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<P>{t('account.youAreNotLoggedIn')}</P>
|
<P>{t('account.youAreNotLoggedIn')}</P>
|
||||||
<P>
|
<ProConnectButton />
|
||||||
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
|
|
||||||
</P>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<HStack
|
<HStack
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type Shortcut = {
|
||||||
|
key: string
|
||||||
|
ctrlKey?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
|
||||||
|
import { isMacintosh } from '@/utils/livekit'
|
||||||
|
import { formatShortcutKey } from './utils'
|
||||||
|
|
||||||
|
export const useKeyboardShortcuts = () => {
|
||||||
|
const shortcutsSnap = useSnapshot(keyboardShortcutsStore)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This approach handles basic shortcuts but isn't comprehensive.
|
||||||
|
// Issues might occur. First draft.
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const { key, metaKey, ctrlKey } = e
|
||||||
|
const shortcutKey = formatShortcutKey({
|
||||||
|
key,
|
||||||
|
ctrlKey: ctrlKey || (isMacintosh() && metaKey),
|
||||||
|
})
|
||||||
|
const shortcut = shortcutsSnap.shortcuts.get(shortcutKey)
|
||||||
|
if (!shortcut) return
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
}, [shortcutsSnap])
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export type useLongPressProps = {
|
||||||
|
keyCode?: string
|
||||||
|
onKeyDown: () => void
|
||||||
|
onKeyUp: () => void
|
||||||
|
longPressThreshold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLongPress = ({
|
||||||
|
keyCode,
|
||||||
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
|
longPressThreshold = 300,
|
||||||
|
}: useLongPressProps) => {
|
||||||
|
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.code != keyCode || timeoutIdRef.current) return
|
||||||
|
timeoutIdRef.current = setTimeout(() => {
|
||||||
|
onKeyDown()
|
||||||
|
}, longPressThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.code != keyCode || !timeoutIdRef.current) return
|
||||||
|
clearTimeout(timeoutIdRef.current)
|
||||||
|
timeoutIdRef.current = null
|
||||||
|
onKeyUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keyCode) return
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
window.addEventListener('keyup', handleKeyUp)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
window.removeEventListener('keyup', handleKeyUp)
|
||||||
|
}
|
||||||
|
}, [keyCode, onKeyDown, onKeyUp, longPressThreshold])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useLongPress
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
|
||||||
|
import { formatShortcutKey } from '@/features/shortcuts/utils'
|
||||||
|
import { Shortcut } from '@/features/shortcuts/types'
|
||||||
|
|
||||||
|
export type useRegisterKeyboardShortcutProps = {
|
||||||
|
shortcut?: Shortcut
|
||||||
|
handler: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRegisterKeyboardShortcut = ({
|
||||||
|
shortcut,
|
||||||
|
handler,
|
||||||
|
}: useRegisterKeyboardShortcutProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shortcut) return
|
||||||
|
keyboardShortcutsStore.shortcuts.set(formatShortcutKey(shortcut), handler)
|
||||||
|
}, [handler, shortcut])
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { isMacintosh } from '@/utils/livekit'
|
||||||
|
import { Shortcut } from '@/features/shortcuts/types'
|
||||||
|
|
||||||
|
export const CTRL = 'ctrl'
|
||||||
|
|
||||||
|
export const formatShortcutKey = (shortcut: Shortcut) => {
|
||||||
|
if (shortcut.ctrlKey) return `${CTRL}+${shortcut.key.toUpperCase()}`
|
||||||
|
return shortcut.key.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appendShortcutLabel = (label: string, shortcut: Shortcut) => {
|
||||||
|
if (!shortcut.key) return
|
||||||
|
let formattedKeyLabel = shortcut.key.toLowerCase()
|
||||||
|
if (shortcut.ctrlKey) {
|
||||||
|
formattedKeyLabel = `${isMacintosh() ? '⌘' : 'Ctrl'}+${formattedKeyLabel}`
|
||||||
|
}
|
||||||
|
return `${label} (${formattedKeyLabel})`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Crisp } from 'crisp-sdk-web'
|
||||||
|
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
|
|
||||||
|
export const initializeSupportSession = (user: ApiUser) => {
|
||||||
|
if (!Crisp.isCrispInjected()) return
|
||||||
|
const { id, email } = user
|
||||||
|
Crisp.setTokenId(`meet-${id}`)
|
||||||
|
Crisp.user.setEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const terminateSupportSession = () => {
|
||||||
|
if (!Crisp.isCrispInjected()) return
|
||||||
|
Crisp.setTokenId()
|
||||||
|
Crisp.session.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type useSupportProps = {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure Crisp chat for real-time support across all pages.
|
||||||
|
export const useSupport = ({ id }: useSupportProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id || Crisp.isCrispInjected()) return
|
||||||
|
Crisp.configure(id)
|
||||||
|
Crisp.setHideOnMobile(true)
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -2,13 +2,16 @@ import { Link } from 'wouter'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { Stack } from '@/styled-system/jsx'
|
import { Stack } from '@/styled-system/jsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { A, Text, Button } from '@/primitives'
|
import { Text, Button } from '@/primitives'
|
||||||
import { SettingsButton } from '@/features/settings'
|
import { SettingsButton } from '@/features/settings'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { logoutUrl, useUser } from '@/features/auth'
|
||||||
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||||
import { Feedback } from '@/components/Feedback'
|
import { Feedback } from '@/components/Feedback'
|
||||||
import { Menu } from '@/primitives/Menu'
|
import { Menu } from '@/primitives/Menu'
|
||||||
import { MenuList } from '@/primitives/MenuList'
|
import { MenuList } from '@/primitives/MenuList'
|
||||||
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
|
import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
|
||||||
|
import { terminateSupportSession } from '@/features/support/hooks/useSupport'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -63,7 +66,7 @@ export const Header = () => {
|
|||||||
<nav>
|
<nav>
|
||||||
<Stack gap={1} direction="row" align="center">
|
<Stack gap={1} direction="row" align="center">
|
||||||
{isLoggedIn === false && !isHome && (
|
{isLoggedIn === false && !isHome && (
|
||||||
<A href={authUrl()}>{t('login')}</A>
|
<ProConnectButton hint={false} />
|
||||||
)}
|
)}
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<Menu>
|
<Menu>
|
||||||
@@ -79,6 +82,8 @@ export const Header = () => {
|
|||||||
items={[{ value: 'logout', label: t('logout') }]}
|
items={[{ value: 'logout', label: t('logout') }]}
|
||||||
onAction={(value) => {
|
onAction={(value) => {
|
||||||
if (value === 'logout') {
|
if (value === 'logout') {
|
||||||
|
terminateAnalyticsSession()
|
||||||
|
terminateSupportSession()
|
||||||
window.location.href = logoutUrl()
|
window.location.href = logoutUrl()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "",
|
"backToHome": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"closeDialog": "",
|
"closeDialog": "",
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "",
|
"loading": "",
|
||||||
"loggedInUserTooltip": "",
|
"loggedInUserTooltip": "",
|
||||||
"login": "Anmelden",
|
"login": {
|
||||||
|
"buttonLabel": "",
|
||||||
|
"linkLabel": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
"logout": "",
|
"logout": "",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": ""
|
"heading": ""
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
"heading": ""
|
"heading": ""
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "",
|
"videoinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
"heading": "",
|
"heading": "",
|
||||||
"joinLabel": "",
|
"joinLabel": "",
|
||||||
"micLabel": "",
|
"joinMeeting": "",
|
||||||
"userLabel": ""
|
"toggleOff": "",
|
||||||
|
"toggleOn": "",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "",
|
||||||
|
"usernameLabel": ""
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "",
|
"leaveRoomPrompt": "",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -31,12 +47,25 @@
|
|||||||
"stopScreenShare": "",
|
"stopScreenShare": "",
|
||||||
"chat": {
|
"chat": {
|
||||||
"open": "",
|
"open": "",
|
||||||
"closed": ""
|
"closed": "",
|
||||||
|
"input": {
|
||||||
|
"textArea": {
|
||||||
|
"label": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hand": {
|
"hand": {
|
||||||
"raise": "",
|
"raise": "",
|
||||||
"lower": ""
|
"lower": ""
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "",
|
||||||
|
"stop": ""
|
||||||
|
},
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "",
|
"open": "",
|
||||||
@@ -49,13 +78,40 @@
|
|||||||
"feedbacks": "",
|
"feedbacks": "",
|
||||||
"support": "",
|
"support": "",
|
||||||
"settings": "",
|
"settings": "",
|
||||||
"username": ""
|
"username": "",
|
||||||
|
"effects": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"participants": {
|
"effects": {
|
||||||
|
"activateCamera": "",
|
||||||
|
"notAvailable": "",
|
||||||
"heading": "",
|
"heading": "",
|
||||||
|
"blur": {
|
||||||
|
"light": "",
|
||||||
|
"normal": "",
|
||||||
|
"apply": "",
|
||||||
|
"clear": ""
|
||||||
|
},
|
||||||
|
"experimental": ""
|
||||||
|
},
|
||||||
|
"sidePanel": {
|
||||||
|
"heading": {
|
||||||
|
"participants": "",
|
||||||
|
"effects": "",
|
||||||
|
"chat": ""
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"participants": "",
|
||||||
|
"effects": "",
|
||||||
|
"chat": ""
|
||||||
|
},
|
||||||
|
"closeButton": ""
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"disclaimer": ""
|
||||||
|
},
|
||||||
|
"participants": {
|
||||||
"subheading": "",
|
"subheading": "",
|
||||||
"closeButton": "",
|
|
||||||
"contributors": "",
|
"contributors": "",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
"open": "",
|
"open": "",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "Back to homescreen",
|
"backToHome": "Back to homescreen",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"closeDialog": "Close dialog",
|
"closeDialog": "Close dialog",
|
||||||
"error": {
|
"error": {
|
||||||
"heading": "An error occured while loading the page"
|
"heading": "An error occurred while loading the page"
|
||||||
},
|
},
|
||||||
"feedbackAlert": "Give us feedback",
|
"feedbackAlert": "Give us feedback",
|
||||||
"forbidden": {
|
"forbidden": {
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"loggedInUserTooltip": "Logged in as…",
|
"loggedInUserTooltip": "Logged in as…",
|
||||||
"login": "Login",
|
"login": {
|
||||||
|
"buttonLabel": "Login with ProConnect",
|
||||||
|
"linkLabel": "What is ProConnect? - new tab",
|
||||||
|
"link": "What is ProConnect?"
|
||||||
|
},
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": "Page not found"
|
"heading": "Page not found"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"createMeeting": "Create a meeting",
|
"createMeeting": "Create a meeting",
|
||||||
"heading": "Welcome in Meet",
|
"heading": "Welcome in Visio",
|
||||||
"intro": "Work easily, from anywhere.",
|
"intro": "Work easily, from anywhere.",
|
||||||
"joinInputError": "Use a meeting link or code. Examples:",
|
"joinInputError": "Use a meeting link or code. Examples:",
|
||||||
"joinInputExample": "URL or 10-letter code",
|
"joinInputExample": "URL or 10-letter code",
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
{
|
{
|
||||||
"feedback": {
|
"feedback": {
|
||||||
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
|
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
|
||||||
"heading": "Help us improve Meet"
|
"heading": "Help us improve Visio"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Camera",
|
"videoinput": {
|
||||||
"heading": "Join the meeting",
|
"choose": "Select camera",
|
||||||
|
"disable": "Disable camera",
|
||||||
|
"enable": "Enable camera",
|
||||||
|
"label": "Camera",
|
||||||
|
"placeholder": "Enable camera to see the preview"
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "Select microphone",
|
||||||
|
"disable": "Disable microphone",
|
||||||
|
"enable": "Enable microphone",
|
||||||
|
"label": "Microphone"
|
||||||
|
},
|
||||||
|
"heading": "Verify your settings",
|
||||||
"joinLabel": "Join",
|
"joinLabel": "Join",
|
||||||
"micLabel": "Microphone",
|
"joinMeeting": "Join meeting",
|
||||||
"userLabel": "Your name"
|
"toggleOff": "Click to turn off",
|
||||||
|
"toggleOn": "Click to turn on",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Shown to other participants",
|
||||||
|
"usernameLabel": "Your name"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -27,16 +43,27 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"shareScreen": "Share screen",
|
|
||||||
"stopScreenShare": "Stop screen share",
|
|
||||||
"chat": {
|
"chat": {
|
||||||
"open": "Close the chat",
|
"open": "Close the chat",
|
||||||
"closed": "Open the chat"
|
"closed": "Open the chat",
|
||||||
|
"input": {
|
||||||
|
"textArea": {
|
||||||
|
"label": "Enter a message",
|
||||||
|
"placeholder": "Enter a message"
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"label": "Send message"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hand": {
|
"hand": {
|
||||||
"raise": "Raise hand",
|
"raise": "Raise hand",
|
||||||
"lower": "Lower hand"
|
"lower": "Lower hand"
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "Share screen",
|
||||||
|
"stop": "Stop screen share"
|
||||||
|
},
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "Hide everyone",
|
"open": "Hide everyone",
|
||||||
@@ -49,13 +76,40 @@
|
|||||||
"feedbacks": "Give us feedbacks",
|
"feedbacks": "Give us feedbacks",
|
||||||
"support": "Get Help on Tchap",
|
"support": "Get Help on Tchap",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"username": "Update Your Name"
|
"username": "Update Your Name",
|
||||||
|
"effects": "Apply effects"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"effects": {
|
||||||
|
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
|
||||||
|
"notAvailable": "The blur effect will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome :(",
|
||||||
|
"heading": "Blur",
|
||||||
|
"blur": {
|
||||||
|
"light": "Light blur",
|
||||||
|
"normal": "Blur",
|
||||||
|
"apply": "Enable blur",
|
||||||
|
"clear": "Disable blur"
|
||||||
|
},
|
||||||
|
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
|
||||||
|
},
|
||||||
|
"sidePanel": {
|
||||||
|
"heading": {
|
||||||
|
"participants": "Participants",
|
||||||
|
"effects": "Effects",
|
||||||
|
"chat": "Messages in the chat"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"participants": "participants",
|
||||||
|
"effects": "effects",
|
||||||
|
"chat": "messages"
|
||||||
|
},
|
||||||
|
"closeButton": "Hide {{content}}"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
|
||||||
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "Participants",
|
|
||||||
"subheading": "In room",
|
"subheading": "In room",
|
||||||
"closeButton": "Hide participants",
|
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"contributors": "Contributors",
|
"contributors": "Contributors",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
|
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
|
||||||
"heading": "Account",
|
"heading": "Account",
|
||||||
"youAreNotLoggedIn": "You are not logged in.",
|
"youAreNotLoggedIn": "You are not logged in.",
|
||||||
"nameLabel": "Votre Nom",
|
"nameLabel": "Your Name",
|
||||||
"authentication": "Authentication"
|
"authentication": "Authentication"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "Meet",
|
"app": "Visio",
|
||||||
"backToHome": "Retour à l'accueil",
|
"backToHome": "Retour à l'accueil",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"closeDialog": "Fermer la fenêtre modale",
|
"closeDialog": "Fermer la fenêtre modale",
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
},
|
},
|
||||||
"loading": "Chargement…",
|
"loading": "Chargement…",
|
||||||
"loggedInUserTooltip": "Connecté en tant que…",
|
"loggedInUserTooltip": "Connecté en tant que…",
|
||||||
"login": "Se connecter",
|
"login": {
|
||||||
|
"buttonLabel": "S'identifier avec ProConnect",
|
||||||
|
"linkLabel": "Qu'est-ce que ProConnect ? - nouvelle fenêtre",
|
||||||
|
"link": "Qu'est-ce que ProConnect ?"
|
||||||
|
},
|
||||||
"logout": "Se déconnecter",
|
"logout": "Se déconnecter",
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"heading": "Page introuvable"
|
"heading": "Page introuvable"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"createMeeting": "Créer une réunion",
|
"createMeeting": "Créer une réunion",
|
||||||
"heading": "Meet",
|
"heading": "Visio",
|
||||||
"intro": "Collaborez en toute simplicité, où que vous soyez.",
|
"intro": "Collaborez en toute simplicité, où que vous soyez.",
|
||||||
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
||||||
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
|
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
{
|
{
|
||||||
"feedback": {
|
"feedback": {
|
||||||
"body": "Remplissez le formulaire disponible dans l'entête du site pour nous donner votre avis sur l'outil. Vos retours sont précieux ! Merci.",
|
"body": "Remplissez le formulaire disponible dans l'entête du site pour nous donner votre avis sur l'outil. Vos retours sont précieux ! Merci.",
|
||||||
"heading": "Aidez-nous à améliorer Meet"
|
"heading": "Aidez-nous à améliorer Visio"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Webcam",
|
"videoinput": {
|
||||||
"heading": "Rejoindre la réunion",
|
"choose": "Choisir la webcam",
|
||||||
|
"disable": "Désactiver la webcam",
|
||||||
|
"enable": "Activer la webcam",
|
||||||
|
"label": "Webcam",
|
||||||
|
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "Choisir le micro",
|
||||||
|
"disable": "Désactiver le micro",
|
||||||
|
"enable": "Activer le micro",
|
||||||
|
"label": "Microphone"
|
||||||
|
},
|
||||||
|
"heading": "Vérifiez vos paramètres",
|
||||||
"joinLabel": "Rejoindre",
|
"joinLabel": "Rejoindre",
|
||||||
"micLabel": "Micro",
|
"joinMeeting": "Rejoindre la réjoindre",
|
||||||
"userLabel": "Votre nom"
|
"toggleOff": "Cliquez pour désactiver",
|
||||||
|
"toggleOn": "Cliquez pour activer",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Affiché aux autres participants",
|
||||||
|
"usernameLabel": "Votre nom"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
@@ -27,16 +43,27 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"shareScreen": "Partager l'écran",
|
|
||||||
"stopScreenShare": "Arrêter le partage",
|
|
||||||
"chat": {
|
"chat": {
|
||||||
"open": "Masquer le chat",
|
"open": "Masquer le chat",
|
||||||
"closed": "Afficher le chat"
|
"closed": "Afficher le chat",
|
||||||
|
"input": {
|
||||||
|
"textArea": {
|
||||||
|
"label": "Ecrire un message",
|
||||||
|
"placeholder": "Ecrire un message"
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"label": "Envoyer un message"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hand": {
|
"hand": {
|
||||||
"raise": "Lever la main",
|
"raise": "Lever la main",
|
||||||
"lower": "Baisser la main"
|
"lower": "Baisser la main"
|
||||||
},
|
},
|
||||||
|
"screenShare": {
|
||||||
|
"start": "Partager l'écran",
|
||||||
|
"stop": "Arrêter le partage"
|
||||||
|
},
|
||||||
"leave": "Quitter",
|
"leave": "Quitter",
|
||||||
"participants": {
|
"participants": {
|
||||||
"open": "Masquer les participants",
|
"open": "Masquer les participants",
|
||||||
@@ -49,13 +76,40 @@
|
|||||||
"feedbacks": "Partager votre avis",
|
"feedbacks": "Partager votre avis",
|
||||||
"support": "Obtenir de l'aide sur Tchap",
|
"support": "Obtenir de l'aide sur Tchap",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"username": "Choisir votre nom"
|
"username": "Choisir votre nom",
|
||||||
|
"effects": "Appliquer des effets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"effects": {
|
||||||
|
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
|
||||||
|
"notAvailable": "L'effet de flou sera bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome :(",
|
||||||
|
"heading": "Flou",
|
||||||
|
"blur": {
|
||||||
|
"light": "Léger flou",
|
||||||
|
"normal": "Flou",
|
||||||
|
"apply": "Appliquer le flou",
|
||||||
|
"clear": "Désactiver le flou"
|
||||||
|
},
|
||||||
|
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet sur tous les navigateurs et une meilleur qualité."
|
||||||
|
},
|
||||||
|
"sidePanel": {
|
||||||
|
"heading": {
|
||||||
|
"participants": "Participants",
|
||||||
|
"effects": "Effets",
|
||||||
|
"chat": "Messages dans l'appel"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"participants": "les participants",
|
||||||
|
"effects": "les effets",
|
||||||
|
"chat": "les messages"
|
||||||
|
},
|
||||||
|
"closeButton": "Masquer {{content}}"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
|
||||||
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "Participants",
|
|
||||||
"subheading": "Dans la réunion",
|
"subheading": "Dans la réunion",
|
||||||
"closeButton": "Masquer les participants",
|
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
"contributors": "Contributeurs",
|
"contributors": "Contributeurs",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button as RACButton,
|
Button as RACButton,
|
||||||
type ButtonProps as RACButtonsProps,
|
type ButtonProps as RACButtonsProps,
|
||||||
Link,
|
|
||||||
LinkProps,
|
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||||
@@ -12,25 +10,12 @@ export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
|||||||
RACButtonsProps &
|
RACButtonsProps &
|
||||||
TooltipWrapperProps
|
TooltipWrapperProps
|
||||||
|
|
||||||
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
|
||||||
LinkProps &
|
|
||||||
TooltipWrapperProps
|
|
||||||
|
|
||||||
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
|
||||||
|
|
||||||
export const Button = ({
|
export const Button = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
tooltipType = 'instant',
|
tooltipType = 'instant',
|
||||||
...props
|
...props
|
||||||
}: ButtonOrLinkProps) => {
|
}: ButtonProps) => {
|
||||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||||
if ((props as LinkButtonProps).href !== undefined) {
|
|
||||||
return (
|
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
|
||||||
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
|
||||||
</TooltipWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
|
|||||||
@@ -134,10 +134,11 @@ export const Checkbox = ({
|
|||||||
>
|
>
|
||||||
<StyledCheckbox {...props}>
|
<StyledCheckbox {...props}>
|
||||||
{(renderProps) => {
|
{(renderProps) => {
|
||||||
renderProps.isInvalid && !!props.validate
|
if (renderProps.isInvalid && !!props.validate) {
|
||||||
? setError(props.validate(renderProps.isSelected))
|
setError(props.validate(renderProps.isSelected))
|
||||||
: setError(null)
|
} else {
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-Checkbox-checkbox">
|
<div className="mt-Checkbox-checkbox">
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Link, LinkProps } from 'react-aria-components'
|
||||||
|
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||||
|
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||||
|
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||||
|
|
||||||
|
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||||
|
LinkProps &
|
||||||
|
TooltipWrapperProps
|
||||||
|
|
||||||
|
export const LinkButton = ({
|
||||||
|
tooltip,
|
||||||
|
tooltipType = 'instant',
|
||||||
|
...props
|
||||||
|
}: LinkButtonProps) => {
|
||||||
|
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
|
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||||
|
</TooltipWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
|
|||||||
const label = typeof item === 'string' ? item : item.label
|
const label = typeof item === 'string' ? item : item.label
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuItemRecipe()}
|
className={menuItemRecipe({ extraPadding: true })}
|
||||||
key={value}
|
key={value}
|
||||||
id={value as string}
|
id={value as string}
|
||||||
onAction={() => {
|
onAction={() => {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
import { Separator as RACSeparator } from 'react-aria-components'
|
||||||
|
|
||||||
|
export const Separator = styled(RACSeparator, {
|
||||||
|
base: {
|
||||||
|
height: '1px',
|
||||||
|
background: 'gray.400',
|
||||||
|
margin: '4px 0',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TextArea as RACTextArea } from 'react-aria-components'
|
||||||
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styled RAC TextArea.
|
||||||
|
*/
|
||||||
|
export const TextArea = styled(RACTextArea, {
|
||||||
|
base: {
|
||||||
|
width: 'full',
|
||||||
|
paddingY: 0.25,
|
||||||
|
paddingX: 0.5,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'control.border',
|
||||||
|
color: 'control.text',
|
||||||
|
borderRadius: 4,
|
||||||
|
transition: 'all 200ms',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
placeholderStyle: {
|
||||||
|
strong: {
|
||||||
|
_placeholder: {
|
||||||
|
color: 'black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -117,6 +117,9 @@ export const buttonRecipe = cva({
|
|||||||
'&[data-pressed]': {
|
'&[data-pressed]': {
|
||||||
borderColor: 'currentcolor',
|
borderColor: 'currentcolor',
|
||||||
},
|
},
|
||||||
|
'&[data-disabled]': {
|
||||||
|
color: 'gray.300',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export { Badge } from './Badge'
|
|||||||
export { Bold } from './Bold'
|
export { Bold } from './Bold'
|
||||||
export { Box } from './Box'
|
export { Box } from './Box'
|
||||||
export { Button } from './Button'
|
export { Button } from './Button'
|
||||||
|
export { LinkButton } from './LinkButton'
|
||||||
export { useCloseDialog } from './useCloseDialog'
|
export { useCloseDialog } from './useCloseDialog'
|
||||||
export { Dialog, type DialogProps } from './Dialog'
|
export { Dialog, type DialogProps } from './Dialog'
|
||||||
export { Div } from './Div'
|
export { Div } from './Div'
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
unreadMessages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatStore = proxy<State>({
|
||||||
|
unreadMessages: 0,
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
egressId: string | undefined
|
||||||
|
egressIsStopping: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const egressStore = proxy<State>({
|
||||||
|
egressId: undefined,
|
||||||
|
egressIsStopping: false,
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
export type KeyboardShortcutHandler = () => void
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
shortcuts: Map<string, KeyboardShortcutHandler>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keyboardShortcutsStore = proxy<State>({
|
||||||
|
shortcuts: new Map<string, KeyboardShortcutHandler>(),
|
||||||
|
})
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { proxy } from 'valtio'
|
import { proxy } from 'valtio'
|
||||||
|
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showHeader: boolean
|
showHeader: boolean
|
||||||
|
activePanelId: PanelId | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layoutStore = proxy<State>({
|
export const layoutStore = proxy<State>({
|
||||||
showHeader: false,
|
showHeader: false,
|
||||||
|
activePanelId: null,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { proxy } from 'valtio'
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
showParticipants: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const participantsStore = proxy<State>({
|
|
||||||
showParticipants: false,
|
|
||||||
})
|
|
||||||
@@ -25,3 +25,7 @@ export function isSafari(): boolean {
|
|||||||
export function isLocal(p: Participant) {
|
export function isLocal(p: Participant) {
|
||||||
return p instanceof LocalParticipant
|
return p instanceof LocalParticipant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMacintosh() {
|
||||||
|
return navigator.platform.indexOf('Mac') > -1
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
djangoSecretKey: ENC[AES256_GCM,data:dqeL5W5WaTir+tzUAIbivT1DE0zv1IVsq1/nTU7LRKHO2PUefwsyGAy37eYIeC89q9I=,iv:ODTaWFvvYLHNYRkIX//wY2knKD2uIwf7Mo4UhUBEX9c=,tag:MnE3wLdLcK79XUwXV99w4w==,type:str]
|
djangoSecretKey: ENC[AES256_GCM,data:p+9m8eNB/dKMXAdfL0cVCg1uKhAv+YLrM+jjajvRYmOZZ9qbiikuFv0dyDp32va/M9w=,iv:ijUztg7ta6BBTsKs+IIfJMFdN0DfzyAKoxlfY8lisPg=,tag:B+uW6akIV0iI2LdMQotrpw==,type:str]
|
||||||
oidc:
|
oidc:
|
||||||
clientId: ENC[AES256_GCM,data:X+Qj//JXn9kfVszbe9BNbL8rQ53AJq0Cluq3nG58OyXlv5/y,iv:/Na7zvcsIu5OxJw1uFwiksh8tXfzG9It26jW41E0edQ=,tag:oph41uyMwj3uZMnZPI64ZQ==,type:str]
|
clientId: ENC[AES256_GCM,data:rHzKkQwFQ7hV6kOBBP60RK41NBKVMUs4dMcZavMQ8gCu9ust,iv:8vviSb+XIKS/zjBIScfmWu0VJ8lXCQZ8p7BxuvJtA2w=,tag:k8vn8I/qxKLE/+JNTDj4Jw==,type:str]
|
||||||
clientSecret: ENC[AES256_GCM,data:2WV+i0pd4wBXiwG8ak+yVROGsmwSaM0X0rEJjFeNeGYtUsHozd5XrurQu6FAB1tQnzTRgRgAU0ob0MqbsMUYhg==,iv:OJZRG+OUD0Fi2HZ0w3Rn+SMFkebMrsQtKvymJkicF28=,tag:PJYzhVvM0gH0xAGrSSPAIQ==,type:str]
|
clientSecret: ENC[AES256_GCM,data:dOYJoG2PStlOMIJPi2exPzsqlxis73iTkcBMvjr8DBr2isWzstpbexscsog7Tuyelw4tpzrJKzC5BTTwJ+xioQ==,iv:oqkLRTPB8+qR0AHvjyNVfHRmoeGrkUvZjrTsWBjIeBc=,tag:hryfmSeqkdWCN9U38jxXlA==,type:str]
|
||||||
#ENC[AES256_GCM,data:iZjWgYcHtDeK/c1TrfVVsZaJRmuc1+2XEJnLEOYw1wXMsUUvgswEc5PP/4UmirhbSulwtuMRhFt74sIogMxPl2etMAoCqo3ziKRuu3FrmgSUD/bdXQRl+VQ0nJ3NnWNcKqf+GS8=,iv:5hUaA4hkABLvXoKVE4TNZevOdUCaCy3zYkGajHpmZ5k=,tag:ikiSAyIly8NAgVA0olASRw==,type:comment]
|
#ENC[AES256_GCM,data:ua1td/VBXGIHDgAw/bm8XnWIRLmgeJKX9dP7g/rNv3jVsXHw6T+iDXxMWpLXNicAZ/RTymdntlwLwsH47r70Z4icEPsjps0yOZ+X734vaL9wVH9IsyFwCihtyck94kgY4CyC7DI=,iv:iGHYu+2aPaI28PQWFheVVuge8BPWLw1VB7Afsz7eLtI=,tag:pfkXsS+/QmHb3kHS/ONHCA==,type:comment]
|
||||||
livekit:
|
livekit:
|
||||||
keys:
|
keys:
|
||||||
devkey: ENC[AES256_GCM,data:deDMTTO9,iv:ZCloCUPRJ7PG2KcGsnhVj3lRDfCmJowwp8PVg4fdTmg=,tag:zCh8lQT29kQbY7LiXHNEiA==,type:str]
|
devkey: ENC[AES256_GCM,data:5RnAMGm3,iv:bY4n8op2KFlXRqzV9h3QwoC3Bws2aEoN1GFxPlrrVBw=,tag:lA+b/6poVRzeJW6Bu8V29A==,type:str]
|
||||||
livekitApi:
|
livekitApi:
|
||||||
key: ENC[AES256_GCM,data:fh0zJSUE,iv:yR5siulW5vlO/Ew8qtYFNNtyODOPm3d3NL4SVCzzVcM=,tag:hLtEuonLJoi0eFJmY0RYEg==,type:str]
|
key: ENC[AES256_GCM,data:JP7KkPms,iv:LlIJ62IRyGf8fByl6abSZv1to2FUc90laC0oL5HFJK4=,tag:2aLMQ79GlDOaiurh8unO0Q==,type:str]
|
||||||
secret: ENC[AES256_GCM,data:81SOHzA6,iv:is7QdB/T58IP41SrPyDdFHtyX57tphb9Gpf/aQ7NnTY=,tag:1E03R+EFYmLtxFoIDcVEHg==,type:str]
|
secret: ENC[AES256_GCM,data:kGDJo1lh,iv:dnI1OuvZGOJZEKZwzoigXqViqYCw/6H7Y0sVXH/p5RA=,tag:G1IB0mc8zuKEmkxrfyImrQ==,type:str]
|
||||||
sops:
|
sops:
|
||||||
kms: []
|
kms: []
|
||||||
gcp_kms: []
|
gcp_kms: []
|
||||||
@@ -18,68 +18,86 @@ sops:
|
|||||||
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZG8rTjAzcVNtVHdLZmJI
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByR3IybDN3eGx4amYzZkFt
|
||||||
Mm4vemNrV1dDUExEZ1R1UGJmWjN3aWw1VXc0CjF1MVhhUTV6ckl1OEFNSjBJUCta
|
OW5VV3FQN3dkSmZBL0JwUE1qSzNLYmRTc1RjCkVCQ2ZmaHk2SFRJaXdMd0VMZUlP
|
||||||
MTE3QU04RDJKMWlWcHhDSG1NTmZyTkEKLS0tIHh5UHRqckUxZWZLUDl4d3FDdHJs
|
b0VQeDVUTDBEZzhBQnhrS2RybzYvL1UKLS0tIG1CbllhWGpsOWx4WEkya0NLeUlC
|
||||||
Y0VMc0llMytmMWNUOW56d3ltTHpwZ2cKEHkn6wuHNeMTk+E6nEMpEJZ6wpdXSi3k
|
WmRScW9MVkxQLzRxdk85WTZ4U2E0aUUKTpOPYQXutU0xYLih7SNYoQgO+PSEIERL
|
||||||
FkzXRa6SudAgA4R6K9EieHKPdiNvi0IsCJOhLpiNQtENu9poF5ozqQ==
|
HLz+C7iV+Fj1/M7JrgiGxTB8wJoKMo7IhJ8AjxaAdxR4Q1TgUpQkPw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAva3lFTTM2ZUJpcnozNmJt
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0aE15QkRsNmg2UTkxaWNF
|
||||||
ZHAvRndkSjRCVEZYN29wMWFZVmV5ekNiYW5FCkI3RWUvVTdVZ2dJeWVSY085SW5G
|
T3NZY2RqSDd0WlRKOHYxWFE2R3J5SGJhRjJNClNIcEFwOEtoSmRWQjdaSm1ZSnlj
|
||||||
UWNOempRQlFCUUlmL0RIR1ZZZVI5cDQKLS0tIEpwZkVodENLM1VnQ1o1TmhRSFFi
|
amhNci9tRDl2Qlp4dlBGZFYzTGxYdm8KLS0tIDZZWTYxQmVqOEZQaTNOODFGWUhn
|
||||||
ZEF5MUNVdHRKcW9aK0M2M0ZVNDBxbjgKw8Wp06PcoStrO6ppsOR5zWjXbYrP64Dv
|
cXpJL3poT3dpYjZKWTN6dGpOV3kxT2sKozsOz+cSYJdZ0C2L6QCf/VSU9DnOz6ae
|
||||||
XAEHQMa7vyvuu12YIa/fpyDM4HrsrPq3OWudxuWS6p0X8xmPYrXm6g==
|
lqV5MMzSl1Jf8ETpqt+PhvvWz+MLCAkIriT9yf6R29DQifCacB7XOA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1OU9mZHZFanpTQjE3aGly
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBySkpOYWxjQVZRbGtkNXlt
|
||||||
VVZDWFh5ZmRHV1YwWUtsZVd6R090SnkxVVNVCmM1UWwwaFlqTHlhSGFRblZBbVhl
|
OTRKTDlrNjNMenU3V0hPeXYyRnhGVU1mMmhNCmhJTi9ZQzB3ekpSR0k1VDFiNExu
|
||||||
aEhtWWp1N1lVTDNqOFJBL0swVFl3Y3cKLS0tIG5ZTHAveFdLdGhQeUc2Tzh1d0dw
|
dW9TQkI3Vy9LOXhQaEExZHMyM25xZlEKLS0tIGRYTkpzbjIvL1FMS2lYYXl4dDVZ
|
||||||
UzRhNGNTZFR3U3BLNFhCdFp6aU5uZVEKL4K5jFjPfMp/fMA8+nQerj6PE5zvGeHW
|
U040akh0Z1ZYVmdjS3k2ZjFRK2VRNGMKqSCnviWARWTkZXeht+sdOYKAxylYYyZK
|
||||||
1SuHwnDiglKmksj8Gy+7spwLQCmo11JCnW2gXKktVIe5XOyhortq3Q==
|
uXYE3nBaXGosIqmTf6deVqCIY+m0mH/J4UMcbH+faMV4pWmVr2JAxg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Z1lXM0tDRzJnMytDZHBV
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJbUhzZStoUVBHUkZLWlE3
|
||||||
ZlpCcTg0U3RGeEVzN2dBMkFYa0FBMFVKMWw4Cm5KeWMvdFBRNHFJWFNSS3NVWlNQ
|
NWNiRkJMdXhUVXRNZTFCMUljVzIxY3BVMFQ4ClpmOGhqeUZiaG1HcU5zdndmWE5y
|
||||||
dnhkc2F2MFVTMmVHT2U2STdUWWcvNzAKLS0tIHkzQXltQW5TWHVrektQU1hES1ll
|
Ym5OTmoyVVVsb2Ywa3loRTVNZzdlVjQKLS0tIHNEWVV3Mkk2VGVzR3diQW5Ccm1a
|
||||||
UnVld1laTWRmTlkzWHRKRjJmWVJhbU0KANxoSnhDbxsja+Eo8MVCGv6iThNmlo/m
|
MVNUYjZCME9rQWFUaWNycEh5THQyTTAKTBnoF76mJ/GoCIq4TsmV+luYbiWnx0+I
|
||||||
y0RXVNVqNlqTreT0F/SKmP74W3lF30nwsfrOywMQyu75k8p5MGwUuQ==
|
BEISvqsr9gbT0z8kfdo/htPoKHZmnyevZhRhd2AMZdKixYvQMX9sjA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwUm9oT3RYbGZXaW5CM20r
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDWEZkODBNOGw2WFdncjJ0
|
||||||
QlYyeGZjMG05MFNzdndCU2dUemZqdXF5Um1FCkZWdVlubW1MZ3NwM3V2S3dQRFA1
|
TTVzRHlEa1AzaTF4V2hYR3hFRGg2cnBzYmowCmp3WDJ3bEZoTlFYL2hoZ3hhTVU1
|
||||||
cHVvOXBMMGtMdkF4T2s3Wk9wYkJUZUEKLS0tIFBRM3FWYTRrVDNwZStwQWlpbEYx
|
WnQyYk03K2xmSk00dS92OHNNZnRIL2cKLS0tIEVrbjY4enJBZzdQMjRCRmwwVlRI
|
||||||
dXA5WE5udnhhMVdLTk1jMzJuU3VWaTQKaxTpyqi5fjmOFR4qOxm+wSqWDxb/96QI
|
OHVOMm9NTGdJbnZ2aXYxdi9OdWpkVE0K4b1Hu6rOHVtfH601aXb/uTGYjNMh6yW/
|
||||||
rHGUI+CqMVKZ6w1fMH0uanvCuw7Q9rbmsKB+DsjARMLFGqxzP/psUA==
|
LetO+HKk+VEzXHntObK2k/4mTl5I0+OP5H8+PR0jdIUZDpr79iEbgQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMmVOaHV1Unl4UmUrVXEy
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqVE9iMmUwTXE2SHZNdG5P
|
||||||
REJIYWZhQmt3R0R5RFRveTl4Z0JaZlpKNmd3Ckl0dkFUaG90TE5odVh0YVprMVZj
|
Vi9XQ1Jkc1VDamFlakpkZk45ODZ2YnkwYkVBCnNrbktIdkV4UGltcHBUUHlXbjdx
|
||||||
T2tXS0dSRGVVczdhb1dlVlV0L3JZUncKLS0tIGo2RDBPZXNzU2dQQlJhY0NBS2ps
|
Z0QwM3ZKbGI1cDBjL2g2cjdKdElOQjAKLS0tIGxrcTJDa1BWVWcxUS80MmxIMWZH
|
||||||
RW92OFVTS3d2L3UxOXNrVUFSVFI5TXMKn3pdHbXxBccG6Q+gWPVQK/5wiIKkzdhm
|
YjBRMDZJZWlmN1FNaXV5c04yVWtleE0K+nGNyFzqSotFP7My/kUnAgxXGu/ji50K
|
||||||
HiNzezStrkFHf/lsFS2LNgYkfMMzBQ4rJj7oQkrD1Z8j6qRld3zzLg==
|
OGVLYgNvU48rCGck3r9ZrKY1HpQdAY8UMQXECsuO4HgdirNjiZ97Zg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9
|
- recipient: age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzeWN4ZE5Ya3RPV2VKRFdv
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZXZud0dqb0dkQ0E3NnE4
|
||||||
UGQ4QTJUZFovSXpQREhNWXdQbktVUHFQaUhjCmM1dXNxbGdlVjBWNjc5L2E5QlBy
|
SXB0ZENjQk1mb1BHU2R1bW0waDhTYy9OZldVCjVnRTV5d1c3Q2NzcEVRQ3BoL09I
|
||||||
NjI0S251cjZvYU1nQ2swWXVDdjdhSG8KLS0tIHdFNXptN1VBNytFNW40MUsxTGFM
|
T1RPQ3hHT3Y2NFNzWG9EdGM2STR2STgKLS0tIHBvL3RhREFNTVdwUGk3S1B4NWJL
|
||||||
WlVnc3VLY1ZReDFxUVlqWU9rM0xMQ00Kq/ckdP1N4BDBo0pSH9pp36skIacwNm3d
|
TnZpblF1SDdGRlVXM0dEdFAzT1FEMUUK6L8gTv5gt6++A3B7PHyWl+xtBUc8bC6G
|
||||||
utiuCgG16Hqe6YDb3agx81BocuSJ9oMOdpegoztnkkBDDP0F+e/VuQ==
|
53xoJvyyBpaov3HgUAdrN9VHubfEJmrBGgN7DngGgwYPtlhV87M7/w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2024-07-18T13:08:42Z"
|
- recipient: age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m
|
||||||
mac: ENC[AES256_GCM,data:a/uHyw9V/SMIePV9nPf+wJgPg+YDYLJGYy7NMLBrBgCXtBWHHonSNjzdmtjix1bW2y+cU0gMqodrtqR1cJGBmXr4NRY7NJqgLWE9rEdYfG7BnfqsWmvAaTIrSs7QMZWkEic7ys/bXoA5BZoau3olhVqIO2A/iyBtoMU9Hv7hPlo=,iv:gaqSCUbN7cxWPNrFPDTl7xNxpOZL6GY/swD/MDCiRqk=,tag:Oz0f/DyD3KGV/9Rprj/1Xw==,type:str]
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlaXY1VmtDejcwTmUxRVZT
|
||||||
|
YURhMkVPaHNvb0sxT0FYL0pvN3hqclNNcXdRCmxWV3FGeDZTM1VVMVRyalpkVnFJ
|
||||||
|
OGU3Wk9wVVAvejVTdjc1MENPcy9Qc1kKLS0tIGpJQXhZVzV3REc2SFlFSXg0dUo5
|
||||||
|
bjRBaGtJdUFmVUkxeGgwbGYwWjRnNEkKYwzwZ9oOo+C6XD57rkUTO6QADZKzYfSF
|
||||||
|
cFJ7fX0NyZbzxLncyofWa+dlLWLZ3KohIP0doAFngRm+RVsUEVqY5A==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1aFNsL2xvWmI4UTAxREc4
|
||||||
|
NFF3bC9qRTBqS3JrM3B0ZjE5bEtjR0diT0VjClhFNStFU3RydnhvcG9CSmhYM3V4
|
||||||
|
VjZ5c0JQZjRoQXh1R2UyeDMyd2NFMEEKLS0tIDNwWUNzZmlrNGZPbERTeFpoUkxO
|
||||||
|
QnZTWWFMemk5djVNWFRaekVMRkMyUjgKt4dw4BOm3J1Ig6U58NbSjzJbWi3ak/Zq
|
||||||
|
8PX5IW7tq1q5+Qd3adqv3cd9S2aVpqjHyN34fxagmuwfvYXVyQ2GDg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2024-10-02T07:30:09Z"
|
||||||
|
mac: ENC[AES256_GCM,data:BdEiR/7AiTz9eppAGOAarFzUJYEfCZzb0lg8LXaHiXe74B5Ob7Ai+XuBBX+x9QPIFzbLZgVveVSrqymW0wAH9Dv5R+e4spDf5KKdRCr9RADfCXNjYC0N9grZVerM70Ic51Lc1kKDnB2mon01W5Sa77Ei29Jo988yvM8AOlXFvr4=,iv:p7PCazxKNv7YcGX7Kpp2L8wXEFaJO8FajEXcVMzmmWQ=,tag:WJKZOkFZSof6IhcXqc60uQ==,type:str]
|
||||||
pgp: []
|
pgp: []
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.8.1
|
version: 3.9.0
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ backend:
|
|||||||
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
|
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
|
||||||
ANALYTICS_KEY: xwhoIMCZ8PBRjQ2t
|
ANALYTICS_KEY: xwhoIMCZ8PBRjQ2t
|
||||||
ALLOW_UNREGISTERED_ROOMS: False
|
ALLOW_UNREGISTERED_ROOMS: False
|
||||||
|
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
|
||||||
|
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
|
||||||
|
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
@@ -95,3 +97,11 @@ ingress:
|
|||||||
ingressAdmin:
|
ingressAdmin:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: meet.127.0.0.1.nip.io
|
host: meet.127.0.0.1.nip.io
|
||||||
|
|
||||||
|
posthog:
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
ingressAssets:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
image:
|
|
||||||
repository: lasuite/meet-backend
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "v0.1.5"
|
|
||||||
|
|
||||||
backend:
|
|
||||||
migrateJobAnnotations:
|
|
||||||
argocd.argoproj.io/hook: PreSync
|
|
||||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
|
||||||
envVars:
|
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-preprod.beta.numerique.gouv.fr,https://meet-preprod.beta.numerique.gouv.fr
|
|
||||||
DJANGO_CONFIGURATION: Production
|
|
||||||
DJANGO_ALLOWED_HOSTS: meet-preprod.beta.numerique.gouv.fr
|
|
||||||
DJANGO_SUPERUSER_EMAIL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: DJANGO_SUPERUSER_EMAIL
|
|
||||||
DJANGO_SECRET_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: DJANGO_SECRET_KEY
|
|
||||||
DJANGO_SETTINGS_MODULE: meet.settings
|
|
||||||
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
|
|
||||||
DJANGO_SUPERUSER_PASSWORD:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: DJANGO_SUPERUSER_PASSWORD
|
|
||||||
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
|
|
||||||
DJANGO_EMAIL_PORT: 465
|
|
||||||
DJANGO_EMAIL_USE_SSL: True
|
|
||||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
|
||||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
|
|
||||||
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
|
|
||||||
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
|
|
||||||
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
|
|
||||||
OIDC_RP_CLIENT_ID:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: OIDC_RP_CLIENT_ID
|
|
||||||
OIDC_RP_CLIENT_SECRET:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: OIDC_RP_CLIENT_SECRET
|
|
||||||
OIDC_RP_SIGN_ALGO: RS256
|
|
||||||
OIDC_RP_SCOPES: "openid email"
|
|
||||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-preprod.beta.numerique.gouv.fr
|
|
||||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
|
||||||
LOGIN_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
|
|
||||||
LOGIN_REDIRECT_URL_FAILURE: https://meet-preprod.beta.numerique.gouv.fr
|
|
||||||
LOGOUT_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
|
|
||||||
DB_HOST:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: host
|
|
||||||
DB_NAME:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: database
|
|
||||||
DB_USER:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: username
|
|
||||||
DB_PASSWORD:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: password
|
|
||||||
DB_PORT:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: port
|
|
||||||
POSTGRES_USER:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: username
|
|
||||||
POSTGRES_DB:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: database
|
|
||||||
POSTGRES_PASSWORD:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgresql.postgres.libre.sh
|
|
||||||
key: password
|
|
||||||
REDIS_URL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: redis.redis.libre.sh
|
|
||||||
key: url
|
|
||||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
|
||||||
LIVEKIT_API_SECRET:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: LIVEKIT_API_SECRET
|
|
||||||
LIVEKIT_API_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: LIVEKIT_API_KEY
|
|
||||||
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
|
|
||||||
ALLOW_UNREGISTERED_ROOMS: False
|
|
||||||
|
|
||||||
createsuperuser:
|
|
||||||
command:
|
|
||||||
- "/bin/sh"
|
|
||||||
- "-c"
|
|
||||||
- |
|
|
||||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
|
||||||
restartPolicy: Never
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
image:
|
|
||||||
repository: lasuite/meet-frontend
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "v0.1.5"
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
host: meet-preprod.beta.numerique.gouv.fr
|
|
||||||
className: nginx
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
|
|
||||||
ingressAdmin:
|
|
||||||
enabled: true
|
|
||||||
host: meet-preprod.beta.numerique.gouv.fr
|
|
||||||
className: nginx
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
|
|
||||||
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-backend
|
repository: lasuite/meet-backend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.7"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
migrateJobAnnotations:
|
migrateJobAnnotations:
|
||||||
argocd.argoproj.io/hook: PostSync
|
argocd.argoproj.io/hook: PostSync
|
||||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||||
envVars:
|
envVars:
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.numerique.gouv.fr
|
DJANGO_CSRF_TRUSTED_ORIGINS: https://visio.numerique.gouv.fr,https://meet.numerique.gouv.fr
|
||||||
DJANGO_CONFIGURATION: Production
|
DJANGO_CONFIGURATION: Production
|
||||||
DJANGO_ALLOWED_HOSTS: meet.numerique.gouv.fr
|
DJANGO_ALLOWED_HOSTS: visio.numerique.gouv.fr,meet.numerique.gouv.fr
|
||||||
DJANGO_SECRET_KEY:
|
DJANGO_SECRET_KEY:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: backend
|
name: backend
|
||||||
@@ -43,11 +43,11 @@ backend:
|
|||||||
key: OIDC_RP_CLIENT_SECRET
|
key: OIDC_RP_CLIENT_SECRET
|
||||||
OIDC_RP_SIGN_ALGO: RS256
|
OIDC_RP_SIGN_ALGO: RS256
|
||||||
OIDC_RP_SCOPES: "openid email"
|
OIDC_RP_SCOPES: "openid email"
|
||||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.numerique.gouv.fr
|
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio.numerique.gouv.fr
|
||||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||||
LOGIN_REDIRECT_URL: https://meet.numerique.gouv.fr
|
LOGIN_REDIRECT_URL: https://visio.numerique.gouv.fr
|
||||||
LOGIN_REDIRECT_URL_FAILURE: https://meet.numerique.gouv.fr
|
LOGIN_REDIRECT_URL_FAILURE: https://visio.numerique.gouv.fr
|
||||||
LOGOUT_REDIRECT_URL: https://meet.numerique.gouv.fr
|
LOGOUT_REDIRECT_URL: https://visio.numerique.gouv.fr
|
||||||
DB_HOST:
|
DB_HOST:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: postgresql.postgres.libre.sh
|
name: postgresql.postgres.libre.sh
|
||||||
@@ -96,6 +96,26 @@ backend:
|
|||||||
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
|
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
|
||||||
ANALYTICS_KEY: mwuxTKi8o2xzWH54
|
ANALYTICS_KEY: mwuxTKi8o2xzWH54
|
||||||
ALLOW_UNREGISTERED_ROOMS: False
|
ALLOW_UNREGISTERED_ROOMS: False
|
||||||
|
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
|
||||||
|
FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio.numerique.gouv.fr'}"
|
||||||
|
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
|
||||||
|
AWS_S3_ENDPOINT_URL:
|
||||||
|
secretKeyRef:
|
||||||
|
name: impress-media-storage.bucket.libre.sh
|
||||||
|
key: url
|
||||||
|
AWS_S3_ACCESS_KEY_ID:
|
||||||
|
secretKeyRef:
|
||||||
|
name: impress-media-storage.bucket.libre.sh
|
||||||
|
key: accessKey
|
||||||
|
AWS_S3_SECRET_ACCESS_KEY:
|
||||||
|
secretKeyRef:
|
||||||
|
name: impress-media-storage.bucket.libre.sh
|
||||||
|
key: secretKey
|
||||||
|
AWS_STORAGE_BUCKET_NAME:
|
||||||
|
secretKeyRef:
|
||||||
|
name: impress-media-storage.bucket.libre.sh
|
||||||
|
key: bucket
|
||||||
|
AWS_S3_REGION_NAME: local
|
||||||
|
|
||||||
createsuperuser:
|
createsuperuser:
|
||||||
command:
|
command:
|
||||||
@@ -109,20 +129,39 @@ frontend:
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-frontend
|
repository: lasuite/meet-frontend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.5"
|
tag: "v0.1.7"
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: meet.numerique.gouv.fr
|
host: visio.numerique.gouv.fr
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
|
|
||||||
ingressAdmin:
|
ingressAdmin:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: meet.numerique.gouv.fr
|
host: visio.numerique.gouv.fr
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/start
|
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/start
|
||||||
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/auth
|
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/auth
|
||||||
|
|
||||||
|
posthog:
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: product.visio.numerique.gouv.fr
|
||||||
|
className: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
|
nginx.ingress.kubernetes.io/upstream-vhost: eu.i.posthog.com
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: https
|
||||||
|
|
||||||
|
ingressAssets:
|
||||||
|
enabled: true
|
||||||
|
host: product.visio.numerique.gouv.fr
|
||||||
|
className: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
|
nginx.ingress.kubernetes.io/upstream-vhost: eu-assets.i.posthog.com
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: https
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user