mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-21 23:57:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd51c142f6 | |||
| 2af6157265 |
+1
-40
@@ -8,48 +8,9 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- 📈(frontend) track errors when starting or stopping a recording
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- ✨(backend) accept form-urlencoded on the user token endpoint
|
|
||||||
- ✨(summary) configurable s3 region
|
|
||||||
- ⬆️(frontend) upgrade i18next and react-i18next patch versions
|
|
||||||
- ⬆️(frontend) upgrade posthog-js from 1.395.0 to 1.404.1
|
|
||||||
- ⬆️(frontend) upgrade livekit-client and @livekit/components-react
|
|
||||||
- 💄(frontend) increase the blur intensity
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 📝(docs) fix minor typos in comments and docstrings
|
- ✨(frontend) recover from stale lazy-loaded chunks after a deploy
|
||||||
- ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
|
|
||||||
- ⬆️(mail) bump @html-to/text-cli from 0.6.0 to 0.6.1
|
|
||||||
- 🐛(frontend) treat client-initiated connect aborts as events
|
|
||||||
- 🐛(frontend) use state instead of a ref for MoreControls container
|
|
||||||
- 🐛(frontend) stop init_virtual_background from firing on blur updates
|
|
||||||
- 🐛(frontend) hoist mute confirmation dialog to VideoConference level
|
|
||||||
- 🐛(frontend) fix joined notification tile no longer rendering properly
|
|
||||||
|
|
||||||
## [1.27.0] - 2026-08-14
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- 🔥(frontend) drop unused vendored ConnectionObserver
|
|
||||||
- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- 📈(frontend) downgrade unreachable external home URL from error to event
|
|
||||||
- 🐛(frontend) handle 401 responses when syncing user preferences
|
|
||||||
- 🐛(frontend) harden speaker test against missing sinks and play errors
|
|
||||||
- 🐛(frontend) implement hysteresis band for the control bar layout
|
|
||||||
- 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift
|
|
||||||
- 🐛(analytics) filter benign ResizeObserver loop error in Sentry/PostHog
|
|
||||||
- 🐛(frontend) stop reporting screen-share denials as errors
|
|
||||||
- 🐛(frontend) generalize screen-share error modal beyond macOS
|
|
||||||
- 📈(frontend) stop double-reporting media device failures
|
|
||||||
|
|
||||||
## [1.26.0] - 2026-08-12
|
## [1.26.0] - 2026-08-12
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Security is very important to us.
|
Security is very important to us.
|
||||||
|
|
||||||
If you have any issue regarding security, please disclose the information responsibly by submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr
|
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr
|
||||||
|
|
||||||
We appreciate your effort to make Visio more secure.
|
We appreciate your effort to make Visio more secure.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -180,7 +180,7 @@ services:
|
|||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
|
||||||
node:
|
node:
|
||||||
image: node:22
|
image: node:18
|
||||||
user: "${DOCKER_USER:-1000}"
|
user: "${DOCKER_USER:-1000}"
|
||||||
environment:
|
environment:
|
||||||
HOME: /tmp
|
HOME: /tmp
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ server {
|
|||||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
try_files $uri =404;
|
||||||
|
error_page 404 = @asset_missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
location @asset_missing {
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Serve static files
|
# Serve static files
|
||||||
|
|||||||
@@ -50,24 +50,10 @@ paths:
|
|||||||
|
|
||||||
The application must be authorized for the user's email domain.
|
The application must be authorized for the user's email domain.
|
||||||
The returned token expires after a configured duration and must be refreshed by calling this endpoint again.
|
The returned token expires after a configured duration and must be refreshed by calling this endpoint again.
|
||||||
|
|
||||||
Request parameters may be sent either as "application/x-www-form-urlencoded"
|
|
||||||
(as specified by RFC 6749 for OAuth 2.0 token endpoints) or as "application/json".
|
|
||||||
operationId: generateToken
|
operationId: generateToken
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TokenRequest'
|
|
||||||
examples:
|
|
||||||
tokenRequest:
|
|
||||||
summary: Request token for user delegation
|
|
||||||
value:
|
|
||||||
client_id: "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
client_secret: "1234567890abcdefghijklmnopqrstuvwxyz"
|
|
||||||
grant_type: "client_credentials"
|
|
||||||
scope: "user@example.com"
|
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/TokenRequest'
|
$ref: '#/components/schemas/TokenRequest'
|
||||||
@@ -131,19 +117,6 @@ paths:
|
|||||||
summary: Domain not authorized
|
summary: Domain not authorized
|
||||||
value:
|
value:
|
||||||
error: "This application is not authorized for this email domain."
|
error: "This application is not authorized for this email domain."
|
||||||
'415':
|
|
||||||
description: |
|
|
||||||
Unsupported media type. The request body must be sent as
|
|
||||||
"application/x-www-form-urlencoded" or "application/json".
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Error'
|
|
||||||
examples:
|
|
||||||
unsupportedMediaType:
|
|
||||||
summary: Unsupported request content type
|
|
||||||
value:
|
|
||||||
detail: 'Unsupported media type "text/plain" in request.'
|
|
||||||
|
|
||||||
/rooms:
|
/rooms:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.27.0"
|
version = "1.26.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.7",
|
"livekit-agents==1.6.7",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.27.0"
|
version = "1.26.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "livekit-agents" },
|
{ name = "livekit-agents" },
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ from rest_framework import decorators, mixins, viewsets
|
|||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
exceptions as drf_exceptions,
|
exceptions as drf_exceptions,
|
||||||
)
|
)
|
||||||
from rest_framework import (
|
|
||||||
parsers as drf_parsers,
|
|
||||||
)
|
|
||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
response as drf_response,
|
response as drf_response,
|
||||||
)
|
)
|
||||||
@@ -44,7 +41,6 @@ class ApplicationViewSet(viewsets.ViewSet):
|
|||||||
methods=["post"],
|
methods=["post"],
|
||||||
url_path="token",
|
url_path="token",
|
||||||
url_name="token",
|
url_name="token",
|
||||||
parser_classes=[drf_parsers.FormParser, drf_parsers.JSONParser],
|
|
||||||
)
|
)
|
||||||
@FeatureFlag.require("application")
|
@FeatureFlag.require("application")
|
||||||
def generate_jwt_access_token(self, request, *args, **kwargs):
|
def generate_jwt_access_token(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Tests for external API /token endpoint
|
|||||||
# pylint: disable=W0621
|
# pylint: disable=W0621
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import pytest
|
import pytest
|
||||||
@@ -89,155 +88,6 @@ def test_api_applications_generate_token_success(settings):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_form_urlencoded(settings):
|
|
||||||
"""The token endpoint should accept "application/x-www-form-urlencoded"
|
|
||||||
requests, as mandated by RFC 6749 (sections 3.2 and 4.4.2) for OAuth 2.0
|
|
||||||
token endpoints, so that standard OAuth 2.0 client libraries work
|
|
||||||
out of the box."""
|
|
||||||
UserFactory(email="user@example.com")
|
|
||||||
application = ApplicationFactory(
|
|
||||||
is_active=True,
|
|
||||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
|
||||||
)
|
|
||||||
|
|
||||||
plain_secret = "test-secret-123"
|
|
||||||
application.client_secret = plain_secret
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
(
|
|
||||||
f"client_id={application.client_id}"
|
|
||||||
f"&client_secret={plain_secret}"
|
|
||||||
"&grant_type=client_credentials"
|
|
||||||
"&scope=user%40example.com"
|
|
||||||
),
|
|
||||||
content_type="application/x-www-form-urlencoded",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert "access_token" in response.data
|
|
||||||
|
|
||||||
response.data.pop("access_token")
|
|
||||||
|
|
||||||
assert response.data == {
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": settings.APPLICATION_JWT_EXPIRATION_SECONDS,
|
|
||||||
"scope": "rooms:list rooms:create",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_form_urlencoded_invalid_credentials():
|
|
||||||
"""Invalid credentials sent as form-urlencoded should be parsed and
|
|
||||||
rejected with 401, proving the request body is properly decoded."""
|
|
||||||
user = UserFactory(email="user@example.com")
|
|
||||||
application = ApplicationFactory(is_active=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
urlencode(
|
|
||||||
{
|
|
||||||
"client_id": application.client_id,
|
|
||||||
"client_secret": "wrong-secret",
|
|
||||||
"grant_type": "client_credentials",
|
|
||||||
"scope": user.email,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/x-www-form-urlencoded",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert "Invalid credentials" in str(response.data)
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_form_urlencoded_missing_fields():
|
|
||||||
"""Missing required fields in a form-urlencoded request should return
|
|
||||||
a 400 validation error, like for JSON requests."""
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
urlencode({"grant_type": "client_credentials"}),
|
|
||||||
content_type="application/x-www-form-urlencoded",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
for field in ("client_id", "client_secret", "scope"):
|
|
||||||
assert field in response.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_form_urlencoded_invalid_grant_type():
|
|
||||||
"""An unsupported grant_type sent as form-urlencoded should return 400."""
|
|
||||||
user = UserFactory(email="user@example.com")
|
|
||||||
application = ApplicationFactory(is_active=True)
|
|
||||||
|
|
||||||
plain_secret = "test-secret-123"
|
|
||||||
application.client_secret = plain_secret
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
urlencode(
|
|
||||||
{
|
|
||||||
"client_id": application.client_id,
|
|
||||||
"client_secret": plain_secret,
|
|
||||||
"grant_type": "authorization_code",
|
|
||||||
"scope": user.email,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/x-www-form-urlencoded",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert "grant_type" in response.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_form_urlencoded_special_characters():
|
|
||||||
"""Percent-encoded reserved characters ("&", "=", "+", "%") in the
|
|
||||||
client_secret should survive form-urlencoded decoding."""
|
|
||||||
UserFactory(email="user@example.com")
|
|
||||||
application = ApplicationFactory(
|
|
||||||
is_active=True,
|
|
||||||
scopes=[ApplicationScope.ROOMS_LIST],
|
|
||||||
)
|
|
||||||
|
|
||||||
plain_secret = "s3cr3t&with=special+chars%42"
|
|
||||||
application.client_secret = plain_secret
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
urlencode(
|
|
||||||
{
|
|
||||||
"client_id": application.client_id,
|
|
||||||
"client_secret": plain_secret,
|
|
||||||
"grant_type": "client_credentials",
|
|
||||||
"scope": "user@example.com",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/x-www-form-urlencoded",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert "access_token" in response.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_unsupported_media_type():
|
|
||||||
"""Content types other than JSON and form-urlencoded should still be
|
|
||||||
rejected with 415 Unsupported Media Type."""
|
|
||||||
client = APIClient()
|
|
||||||
response = client.post(
|
|
||||||
"/external-api/v1.0/application/token/",
|
|
||||||
"client_id=x&client_secret=y&grant_type=client_credentials&scope=a@b.co",
|
|
||||||
content_type="text/plain",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 415
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_applications_generate_token_invalid_client_id():
|
def test_api_applications_generate_token_invalid_client_id():
|
||||||
"""Invalid client_id should return 401."""
|
"""Invalid client_id should return 401."""
|
||||||
user = UserFactory(email="user@example.com")
|
user = UserFactory(email="user@example.com")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.27.0"
|
version = "1.26.0"
|
||||||
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",
|
||||||
|
|||||||
Generated
+4
-4
@@ -1187,7 +1187,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.27.0"
|
version = "1.26.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -2252,11 +2252,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.6.0"
|
version = "0.5.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ server {
|
|||||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
try_files $uri =404;
|
||||||
|
error_page 404 = @asset_missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
location @asset_missing {
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Serve static files
|
# Serve static files
|
||||||
|
|||||||
Generated
+71
-74
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
"@fontsource/opendyslexic": "5.2.5",
|
"@fontsource/opendyslexic": "5.2.5",
|
||||||
"@libreaudio/la-call": "0.1.4",
|
"@libreaudio/la-call": "0.1.4",
|
||||||
"@livekit/components-react": "2.9.23",
|
"@livekit/components-react": "2.9.21",
|
||||||
"@livekit/components-styles": "1.2.0",
|
"@livekit/components-styles": "1.2.0",
|
||||||
"@livekit/track-processors": "0.7.2",
|
"@livekit/track-processors": "0.7.2",
|
||||||
"@mediapipe/tasks-vision": "0.10.14",
|
"@mediapipe/tasks-vision": "0.10.14",
|
||||||
@@ -24,17 +24,17 @@
|
|||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
"hoofd": "1.7.3",
|
"hoofd": "1.7.3",
|
||||||
"humanize-duration": "3.33.2",
|
"humanize-duration": "3.33.2",
|
||||||
"i18next": "26.3.6",
|
"i18next": "26.3.1",
|
||||||
"i18next-browser-languagedetector": "8.2.1",
|
"i18next-browser-languagedetector": "8.2.1",
|
||||||
"i18next-parser": "9.4.0",
|
"i18next-parser": "9.4.0",
|
||||||
"i18next-resources-to-backend": "1.2.1",
|
"i18next-resources-to-backend": "1.2.1",
|
||||||
"livekit-client": "2.21.0",
|
"livekit-client": "2.20.0",
|
||||||
"posthog-js": "1.404.1",
|
"posthog-js": "1.395.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-aria": "3.50.0",
|
"react-aria": "3.50.0",
|
||||||
"react-aria-components": "1.19.0",
|
"react-aria-components": "1.19.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "17.0.10",
|
"react-i18next": "17.0.8",
|
||||||
"react-stately": "3.48.0",
|
"react-stately": "3.48.0",
|
||||||
"use-sound": "5.0.0",
|
"use-sound": "5.0.0",
|
||||||
"valtio": "2.3.2",
|
"valtio": "2.3.2",
|
||||||
@@ -742,28 +742,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.8.0",
|
"version": "1.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/dom": {
|
|
||||||
"version": "1.7.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
|
||||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.7.5",
|
|
||||||
"@floating-ui/utils": "^0.2.11"
|
"@floating-ui/utils": "^0.2.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.3",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/atkinson-hyperlegible-next": {
|
"node_modules/@fontsource-variable/atkinson-hyperlegible-next": {
|
||||||
@@ -966,12 +966,12 @@
|
|||||||
"license": "GPL-3.0+"
|
"license": "GPL-3.0+"
|
||||||
},
|
},
|
||||||
"node_modules/@livekit/components-core": {
|
"node_modules/@livekit/components-core": {
|
||||||
"version": "0.12.14",
|
"version": "0.12.13",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.14.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
||||||
"integrity": "sha512-6OKP/1Ok2fCZewDLKd3SzaTb7KvfZl/6hjggI+TgUdhp0Y7HBg3+tHA7YmhZRc0BO8wIyVy4VgTPn7qkLHt2nQ==",
|
"integrity": "sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "1.7.6",
|
"@floating-ui/dom": "1.7.4",
|
||||||
"loglevel": "1.9.1",
|
"loglevel": "1.9.1",
|
||||||
"rxjs": "7.8.2"
|
"rxjs": "7.8.2"
|
||||||
},
|
},
|
||||||
@@ -979,17 +979,17 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"livekit-client": "^2.20.1",
|
"livekit-client": "^2.17.2",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@livekit/components-react": {
|
"node_modules/@livekit/components-react": {
|
||||||
"version": "2.9.23",
|
"version": "2.9.21",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.23.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.21.tgz",
|
||||||
"integrity": "sha512-clO+0g/u3YBpuOvnAjUSAJBH/o7w+RpUAseswjiSML9rHrXlTUhwBNm8LPk+qN5GOU3A6lzsNnFYVpUHUBVOkg==",
|
"integrity": "sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/components-core": "0.12.14",
|
"@livekit/components-core": "0.12.13",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
@@ -999,8 +999,8 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0 || ^0.4.0",
|
"@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0",
|
||||||
"livekit-client": "^2.20.1",
|
"livekit-client": "^2.18.2",
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
"react-dom": ">=18",
|
"react-dom": ">=18",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
@@ -1027,9 +1027,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@livekit/protocol": {
|
"node_modules/@livekit/protocol": {
|
||||||
"version": "1.50.4",
|
"version": "1.46.6",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.50.4.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.46.6.tgz",
|
||||||
"integrity": "sha512-L1uggNQAqyY21smQY8AllyOYbcv9Me9TaxwuLytL1R8ck9nbYPmQLNwEDi3pOFGAMa5F8I2nUi2Jc59W5awxlA==",
|
"integrity": "sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^1.10.0"
|
"@bufbuild/protobuf": "^1.10.0"
|
||||||
@@ -1720,18 +1720,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@posthog/core": {
|
"node_modules/@posthog/core": {
|
||||||
"version": "1.48.3",
|
"version": "1.39.5",
|
||||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.48.3.tgz",
|
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.39.5.tgz",
|
||||||
"integrity": "sha512-kwVDVvwtCTXctApA2tpnwDjDDan8LrkwCW1Wv6PABaVs/s5ahbQ9W3pvdXcqr71HCuqukSA1jp7MySczebubGg==",
|
"integrity": "sha512-M8Imv7ZmmrT6derMOFXhX3c5VKKO2oG6OX70ozLnNpKJV73sNIhYmRX/raa90saF7OA0YmWWJlpKje3irGAyQQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@posthog/types": "^1.405.0"
|
"@posthog/types": "^1.392.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@posthog/types": {
|
"node_modules/@posthog/types": {
|
||||||
"version": "1.405.0",
|
"version": "1.392.0",
|
||||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.405.0.tgz",
|
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.392.0.tgz",
|
||||||
"integrity": "sha512-4rZ/taVXKQxs9Jrf7ZjlCRgrOSL69oKAgIWJQa5kRNJ6wll1UANbrJTSY+Su1e88LIG4zZVjKKKjyQHCkHdHcw==",
|
"integrity": "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@react-aria/overlays": {
|
"node_modules/@react-aria/overlays": {
|
||||||
@@ -4393,14 +4393,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.50.0",
|
"version": "3.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||||
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
|
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/core-js"
|
"url": "https://opencollective.com/core-js"
|
||||||
@@ -6403,9 +6400,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/i18next": {
|
"node_modules/i18next": {
|
||||||
"version": "26.3.6",
|
"version": "26.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
|
||||||
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
|
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -6422,7 +6419,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5 || ^6 || ^7"
|
"typescript": "^5 || ^6"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
@@ -8112,20 +8109,20 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/livekit-client": {
|
"node_modules/livekit-client": {
|
||||||
"version": "2.21.0",
|
"version": "2.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.20.0.tgz",
|
||||||
"integrity": "sha512-RBUhPkV/sl1nzl8lokVlK5uATPwn0AlsudCBZXissw/kDl9yz8ac4pNJ43iPpMVoHOeBYH/BZ3vUC1adqa/zFQ==",
|
"integrity": "sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/mutex": "1.1.1",
|
"@livekit/mutex": "1.1.1",
|
||||||
"@livekit/protocol": "1.50.4",
|
"@livekit/protocol": "1.46.6",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jose": "^6.1.0",
|
"jose": "^6.1.0",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"sdp-transform": "^2.15.0",
|
"sdp-transform": "^2.15.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typed-emitter": "^2.1.0",
|
"typed-emitter": "^2.1.0",
|
||||||
"webrtc-adapter": "9.0.6"
|
"webrtc-adapter": "9.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/dom-mediacapture-record": "^1"
|
"@types/dom-mediacapture-record": "^1"
|
||||||
@@ -9123,17 +9120,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/posthog-js": {
|
"node_modules/posthog-js": {
|
||||||
"version": "1.404.1",
|
"version": "1.395.0",
|
||||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.404.1.tgz",
|
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.395.0.tgz",
|
||||||
"integrity": "sha512-ck/HOMKuZ6yefUk5OX+h2s9mUWBsnu0mGDUAWjIwAmQQrloZPShzOhOWuEuZQuMnCKORBFRVGsBAuzsl66cBJA==",
|
"integrity": "sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==",
|
||||||
"license": "(Apache-2.0 AND MIT)",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@posthog/core": "^1.43.1",
|
"@posthog/core": "^1.38.0",
|
||||||
"@posthog/types": "^1.397.0",
|
"@posthog/types": "^1.391.1",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.38.1",
|
||||||
"dompurify": "^3.3.2",
|
"dompurify": "^3.3.2",
|
||||||
"fflate": "^0.4.8",
|
"fflate": "^0.4.8",
|
||||||
"preact": "^10.29.3",
|
"preact": "^10.29.2",
|
||||||
"query-selector-shadow-dom": "^1.0.1",
|
"query-selector-shadow-dom": "^1.0.1",
|
||||||
"web-vitals": "^5.3.0"
|
"web-vitals": "^5.3.0"
|
||||||
}
|
}
|
||||||
@@ -9425,9 +9422,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-i18next": {
|
"node_modules/react-i18next": {
|
||||||
"version": "17.0.10",
|
"version": "17.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||||
"integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
|
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.29.2",
|
"@babel/runtime": "^7.29.2",
|
||||||
@@ -9437,7 +9434,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"i18next": ">= 26.2.0",
|
"i18next": ">= 26.2.0",
|
||||||
"react": ">= 16.8.0",
|
"react": ">= 16.8.0",
|
||||||
"typescript": "^5 || ^6 || ^7"
|
"typescript": "^5 || ^6"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
@@ -11648,9 +11645,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/webrtc-adapter": {
|
"node_modules/webrtc-adapter": {
|
||||||
"version": "9.0.6",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz",
|
||||||
"integrity": "sha512-CHbl2ZQbxx164IgWRgzJno4hWtM4tFbRam1QfI3Yxhs3w/DvqluVxVWeXs3oL5/fbGkSNLKo0Ty5MgUWceNhog==",
|
"integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sdp": "^3.2.0"
|
"sdp": "^3.2.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
"@fontsource/opendyslexic": "5.2.5",
|
"@fontsource/opendyslexic": "5.2.5",
|
||||||
"@libreaudio/la-call": "0.1.4",
|
"@libreaudio/la-call": "0.1.4",
|
||||||
"@livekit/components-react": "2.9.23",
|
"@livekit/components-react": "2.9.21",
|
||||||
"@livekit/components-styles": "1.2.0",
|
"@livekit/components-styles": "1.2.0",
|
||||||
"@livekit/track-processors": "0.7.2",
|
"@livekit/track-processors": "0.7.2",
|
||||||
"@mediapipe/tasks-vision": "0.10.14",
|
"@mediapipe/tasks-vision": "0.10.14",
|
||||||
@@ -31,17 +31,17 @@
|
|||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
"hoofd": "1.7.3",
|
"hoofd": "1.7.3",
|
||||||
"humanize-duration": "3.33.2",
|
"humanize-duration": "3.33.2",
|
||||||
"i18next": "26.3.6",
|
"i18next": "26.3.1",
|
||||||
"i18next-browser-languagedetector": "8.2.1",
|
"i18next-browser-languagedetector": "8.2.1",
|
||||||
"i18next-parser": "9.4.0",
|
"i18next-parser": "9.4.0",
|
||||||
"i18next-resources-to-backend": "1.2.1",
|
"i18next-resources-to-backend": "1.2.1",
|
||||||
"livekit-client": "2.21.0",
|
"livekit-client": "2.20.0",
|
||||||
"posthog-js": "1.404.1",
|
"posthog-js": "1.395.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-aria": "3.50.0",
|
"react-aria": "3.50.0",
|
||||||
"react-aria-components": "1.19.0",
|
"react-aria-components": "1.19.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "17.0.10",
|
"react-i18next": "17.0.8",
|
||||||
"react-stately": "3.48.0",
|
"react-stately": "3.48.0",
|
||||||
"use-sound": "5.0.0",
|
"use-sound": "5.0.0",
|
||||||
"valtio": "2.3.2",
|
"valtio": "2.3.2",
|
||||||
|
|||||||
@@ -3,30 +3,27 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
import { reportError } from '@/features/analytics/telemetry'
|
||||||
import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput'
|
|
||||||
|
|
||||||
export const SoundTester = () => {
|
export const SoundTester = () => {
|
||||||
const { t } = useTranslation('settings')
|
const { t } = useTranslation('settings')
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
const audioRef = useRef<HTMLAudioElement>(null)
|
||||||
|
|
||||||
const { devices, activeDeviceId } = useMediaDeviceSelect({
|
const { activeDeviceId } = useMediaDeviceSelect({ kind: 'audiooutput' })
|
||||||
kind: 'audiooutput',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canTestAudioOutput() || !activeDeviceId) return
|
const updateActiveId = async (deviceId: string) => {
|
||||||
if (!devices.some((device) => device.deviceId === activeDeviceId)) return
|
try {
|
||||||
audioRef.current?.setSinkId(activeDeviceId).catch((error) => {
|
await audioRef?.current?.setSinkId(deviceId)
|
||||||
if (error instanceof DOMException && error.name === 'NotFoundError') {
|
} catch (error) {
|
||||||
return
|
reportError(
|
||||||
|
'device_switch_failure',
|
||||||
|
new Error(`Error setting sinkId: ${error}`)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
reportError(
|
}
|
||||||
'device_switch_failure',
|
updateActiveId(activeDeviceId)
|
||||||
new Error(`Error setting sinkId: ${error}`)
|
}, [activeDeviceId])
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [devices, activeDeviceId])
|
|
||||||
|
|
||||||
// prevent pausing the sound
|
// prevent pausing the sound
|
||||||
navigator.mediaSession.setActionHandler('pause', function () {})
|
navigator.mediaSession.setActionHandler('pause', function () {})
|
||||||
@@ -35,13 +32,9 @@ export const SoundTester = () => {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="secondaryText"
|
variant="secondaryText"
|
||||||
onPress={async () => {
|
onPress={() => {
|
||||||
try {
|
audioRef?.current?.play()
|
||||||
await audioRef?.current?.play()
|
setIsPlaying(true)
|
||||||
setIsPlaying(true)
|
|
||||||
} catch {
|
|
||||||
setIsPlaying(false)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={isPlaying}
|
isDisabled={isPlaying}
|
||||||
@@ -55,7 +48,7 @@ export const SoundTester = () => {
|
|||||||
{/* eslint-disable jsx-a11y/media-has-caption */}
|
{/* eslint-disable jsx-a11y/media-has-caption */}
|
||||||
<audio
|
<audio
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
src="/sounds/uprise.mp3"
|
src="sounds/uprise.mp3"
|
||||||
onEnded={() => setIsPlaying(false)}
|
onEnded={() => setIsPlaying(false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { CaptureResult } from 'posthog-js'
|
|
||||||
|
|
||||||
const IGNORED_EXCEPTION_PATTERNS = [
|
|
||||||
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/,
|
|
||||||
]
|
|
||||||
|
|
||||||
const shouldIgnoreException = (value: unknown): boolean =>
|
|
||||||
typeof value === 'string' &&
|
|
||||||
IGNORED_EXCEPTION_PATTERNS.some((pattern) => pattern.test(value))
|
|
||||||
|
|
||||||
export const filterExceptions = (
|
|
||||||
event: CaptureResult | null
|
|
||||||
): CaptureResult | null => {
|
|
||||||
if (event?.event !== '$exception') return event
|
|
||||||
|
|
||||||
const exceptionList = event.properties?.['$exception_list']
|
|
||||||
const values: unknown[] = Array.isArray(exceptionList)
|
|
||||||
? exceptionList.map((exception) => exception?.value)
|
|
||||||
: []
|
|
||||||
|
|
||||||
values.push(event.properties?.['$exception_message'])
|
|
||||||
|
|
||||||
return values.some(shouldIgnoreException) ? null : event
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { useEffect } from 'react'
|
|||||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
import { useUser } from '@/features/auth/api/useUser'
|
||||||
import { getPosthog } from '../utils'
|
import { getPosthog } from '../utils'
|
||||||
import { filterExceptions } from '../exceptionFilters'
|
|
||||||
|
|
||||||
export const startAnalyticsSession = (data: ApiUser) => {
|
export const startAnalyticsSession = (data: ApiUser) => {
|
||||||
getPosthog().then((ph) => {
|
getPosthog().then((ph) => {
|
||||||
@@ -48,7 +47,6 @@ export const useAnalytics = ({
|
|||||||
capture_unhandled_rejections: true,
|
capture_unhandled_rejections: true,
|
||||||
capture_console_errors: true,
|
capture_console_errors: true,
|
||||||
},
|
},
|
||||||
before_send: filterExceptions,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [id, host, flags_api_host, isDisabled])
|
}, [id, host, flags_api_host, isDisabled])
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export type LogCode =
|
|||||||
| 'livekit_room_error'
|
| 'livekit_room_error'
|
||||||
| 'device_switch_failure'
|
| 'device_switch_failure'
|
||||||
| 'permission_poll_failure'
|
| 'permission_poll_failure'
|
||||||
| 'media_devices_error_event'
|
|
||||||
// non-media families
|
// non-media families
|
||||||
| 'participant_mute_api_failure'
|
| 'participant_mute_api_failure'
|
||||||
| 'permissions_api_failure'
|
| 'permissions_api_failure'
|
||||||
@@ -138,7 +137,6 @@ export const captureMediaEvent = async (
|
|||||||
| 'media-device-success'
|
| 'media-device-success'
|
||||||
| 'device-not-found'
|
| 'device-not-found'
|
||||||
| 'permissions-denied'
|
| 'permissions-denied'
|
||||||
| 'screen-share-permission-denied'
|
|
||||||
| 'silent-mic-detected'
|
| 'silent-mic-detected'
|
||||||
| 'silent-mic-analyser-unavailable'
|
| 'silent-mic-analyser-unavailable'
|
||||||
| 'silent-mic-recovered'
|
| 'silent-mic-recovered'
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { queryClient } from '@/api/queryClient'
|
|||||||
import { updateUserPreferences } from './updateUserPreferences'
|
import { updateUserPreferences } from './updateUserPreferences'
|
||||||
import { convertToBackendLanguage } from '@/utils/languages'
|
import { convertToBackendLanguage } from '@/utils/languages'
|
||||||
import { useUser } from './useUser'
|
import { useUser } from './useUser'
|
||||||
import { ApiError } from '@/api/ApiError.ts'
|
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that synchronizes user browser preferences (language, timezone) with backend user settings.
|
* Hook that synchronizes user browser preferences (language, timezone) with backend user settings.
|
||||||
@@ -44,11 +42,6 @@ export const useSyncUserPreferencesWithBackend = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncBrowserPreferencesToBackend().catch((error) => {
|
syncBrowserPreferencesToBackend()
|
||||||
if (error instanceof ApiError && error.statusCode === 401) return
|
|
||||||
reportError('generic_failure', error, {
|
|
||||||
context: '[useSyncUserPreferencesWithBackend] Failed to sync:',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [i18n.language, isLoggedIn, user, mutateAsync])
|
}, [i18n.language, isLoggedIn, user, mutateAsync])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChatRow } from '@/stores/chat'
|
import { ChatRow } from '@/stores/chat'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { formatChatMessageLinks } from '../utils'
|
import { formatChatMessageLinks } from '@livekit/components-react'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { Text } from '@/primitives'
|
import { Text } from '@/primitives'
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ChatProvider = () => {
|
|||||||
resetChatStore()
|
resetChatStore()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Trigger the message notification (temporary)
|
// Tigger the message notification (temporary)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TEMPORARY: This is a brittle workaround that relies on message count tracking
|
// TEMPORARY: This is a brittle workaround that relies on message count tracking
|
||||||
// due to recent LiveKit useChat changes breaking the previous implementation
|
// due to recent LiveKit useChat changes breaking the previous implementation
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { tokenize, createDefaultGrammar } from '@livekit/components-core'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
const defaultGrammar = Object.freeze(createDefaultGrammar())
|
|
||||||
|
|
||||||
export function formatChatMessageLinks(message: string): ReactNode {
|
|
||||||
const trimmedMessage = message.replace(/^[\r\n]+|[\r\n]+$/g, '')
|
|
||||||
return tokenize(trimmedMessage, defaultGrammar).map((tok, i) => {
|
|
||||||
if (typeof tok === `string`) {
|
|
||||||
return tok
|
|
||||||
} else {
|
|
||||||
const content = tok.content.toString()
|
|
||||||
const href =
|
|
||||||
tok.type === `url`
|
|
||||||
? /^http(s?):\/\//.test(content)
|
|
||||||
? content
|
|
||||||
: `https://${content}`
|
|
||||||
: `mailto:${content}`
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="lk-chat-link"
|
|
||||||
key={i}
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ import { css } from '@/styled-system/css'
|
|||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
import { reportError } from '@/features/analytics/telemetry'
|
||||||
|
|
||||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -161,10 +161,8 @@ const Home = () => {
|
|||||||
window.location.replace(data.external_home_url)
|
window.location.replace(data.external_home_url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRedirectFailed(true)
|
setRedirectFailed(true)
|
||||||
captureEvent('external-home-unreachable', {
|
reportError('generic_failure', error, {
|
||||||
error_name: error instanceof Error ? error.name : 'Unknown',
|
context: 'Site is not reachable:',
|
||||||
error_message:
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useToast } from 'react-aria'
|
import { useToast } from 'react-aria'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { Button as RACButton } from 'react-aria-components'
|
import { Button as RACButton } from 'react-aria-components'
|
||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
@@ -11,7 +11,6 @@ import { Div } from '@/primitives'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyledToastContainer } from './StyledToastContainer'
|
import { StyledToastContainer } from './StyledToastContainer'
|
||||||
import { setPinnedTrack } from '@/stores/layout'
|
import { setPinnedTrack } from '@/stores/layout'
|
||||||
import { useParticipantTracks } from '@livekit/components-react'
|
|
||||||
|
|
||||||
const ClickableToast = styled(RACButton, {
|
const ClickableToast = styled(RACButton, {
|
||||||
base: {
|
base: {
|
||||||
@@ -31,18 +30,14 @@ export function ToastJoined({ state, ...props }: Readonly<ToastProps>) {
|
|||||||
)
|
)
|
||||||
const participant = props.toast.content.participant
|
const participant = props.toast.content.participant
|
||||||
|
|
||||||
const [cameraTrack] = useParticipantTracks(
|
|
||||||
[Source.Camera],
|
|
||||||
participant?.identity
|
|
||||||
)
|
|
||||||
|
|
||||||
const trackReference = useMemo(
|
|
||||||
() => cameraTrack ?? { participant, source: Source.Camera },
|
|
||||||
[cameraTrack, participant]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!participant) return
|
if (!participant) return
|
||||||
|
|
||||||
|
const trackReference = {
|
||||||
|
participant,
|
||||||
|
publication: participant.getTrackPublication(Source.Camera),
|
||||||
|
source: Source.Camera,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledToastContainer {...toastProps} ref={ref}>
|
<StyledToastContainer {...toastProps} ref={ref}>
|
||||||
<ClickableToast
|
<ClickableToast
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { NotificationType } from '@/features/notifications/NotificationType
|
|||||||
// fixme - handle dynamic audio output changes
|
// fixme - handle dynamic audio output changes
|
||||||
export const useNotificationSound = () => {
|
export const useNotificationSound = () => {
|
||||||
const notificationsSnap = useSnapshot(notificationsStore)
|
const notificationsSnap = useSnapshot(notificationsStore)
|
||||||
const [play] = useSound('/sounds/notifications.mp3', {
|
const [play] = useSound('./sounds/notifications.mp3', {
|
||||||
sprite: {
|
sprite: {
|
||||||
participantJoined: [0, 1150],
|
participantJoined: [0, 1150],
|
||||||
handRaised: [1400, 180],
|
handRaised: [1400, 180],
|
||||||
|
|||||||
+26
-11
@@ -1,9 +1,11 @@
|
|||||||
import { Participant, Track } from 'livekit-client'
|
import { Participant, Track } from 'livekit-client'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useTrackMutedIndicator } from '@livekit/components-react'
|
import { useTrackMutedIndicator } from '@livekit/components-react'
|
||||||
|
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||||
|
import { useState } from 'react'
|
||||||
import { Button } from '@/primitives'
|
import { Button } from '@/primitives'
|
||||||
import { RiMicLine, RiMicOffLine } from '@remixicon/react'
|
import { RiMicLine, RiMicOffLine } from '@remixicon/react'
|
||||||
import { openMuteDialog } from '@/stores/muteDialog'
|
import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
|
||||||
|
|
||||||
export const MuteButton = ({ participant }: { participant: Participant }) => {
|
export const MuteButton = ({ participant }: { participant: Participant }) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
||||||
@@ -13,18 +15,31 @@ export const MuteButton = ({ participant }: { participant: Participant }) => {
|
|||||||
source: Track.Source.Microphone,
|
source: Track.Source.Microphone,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { muteParticipant } = useMuteParticipant()
|
||||||
|
const [isAlertOpen, setIsAlertOpen] = useState(false)
|
||||||
|
|
||||||
const name = participant.name || participant.identity
|
const name = participant.name || participant.identity
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<>
|
||||||
isDisabled={isMuted}
|
<Button
|
||||||
size={'sm'}
|
isDisabled={isMuted}
|
||||||
variant={'primaryTextDark'}
|
size={'sm'}
|
||||||
square
|
variant={'primaryTextDark'}
|
||||||
onPress={() => openMuteDialog(participant)}
|
square
|
||||||
tooltip={t('muteParticipant', { name })}
|
onPress={() => setIsAlertOpen(true)}
|
||||||
>
|
tooltip={t('muteParticipant', { name })}
|
||||||
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
>
|
||||||
</Button>
|
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
||||||
|
</Button>
|
||||||
|
<MuteAlertDialog
|
||||||
|
isOpen={isAlertOpen}
|
||||||
|
onSubmit={() =>
|
||||||
|
muteParticipant(participant).then(() => setIsAlertOpen(false))
|
||||||
|
}
|
||||||
|
onClose={() => setIsAlertOpen(false)}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ import {
|
|||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
|
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
|
||||||
import { Button } from '@/primitives'
|
import { Button } from '@/primitives'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||||
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
|
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
|
||||||
import { ParticipantMenuButton } from './menu/ParticipantMenuButton'
|
import { ParticipantMenuButton } from './menu/ParticipantMenuButton'
|
||||||
import { PinBadge } from './PinBadge'
|
import { PinBadge } from './PinBadge'
|
||||||
import { UnauthenticatedBadge } from './UnauthenticatedBadge'
|
import { UnauthenticatedBadge } from './UnauthenticatedBadge'
|
||||||
import { openMuteDialog } from '@/stores/muteDialog'
|
import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
|
||||||
import { ParticipantName } from './ParticipantName'
|
import { ParticipantName } from './ParticipantName'
|
||||||
|
|
||||||
type MicIndicatorProps = {
|
type MicIndicatorProps = {
|
||||||
@@ -32,6 +34,7 @@ type MicIndicatorProps = {
|
|||||||
|
|
||||||
const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
|
const { muteParticipant } = useMuteParticipant()
|
||||||
const { isMuted } = useTrackMutedIndicator({
|
const { isMuted } = useTrackMutedIndicator({
|
||||||
participant: participant,
|
participant: participant,
|
||||||
source: Source.Microphone,
|
source: Source.Microphone,
|
||||||
@@ -39,6 +42,7 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
|||||||
|
|
||||||
const canMute = useCanMute(participant)
|
const canMute = useCanMute(participant)
|
||||||
const isSpeaking = useIsSpeaking(participant)
|
const isSpeaking = useIsSpeaking(participant)
|
||||||
|
const [isAlertOpen, setIsAlertOpen] = useState(false)
|
||||||
const name = participant.name || participant.identity
|
const name = participant.name || participant.identity
|
||||||
|
|
||||||
const label = isLocal(participant)
|
const label = isLocal(participant)
|
||||||
@@ -48,34 +52,46 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<>
|
||||||
square
|
<Button
|
||||||
variant="greyscale"
|
square
|
||||||
size="sm"
|
variant="greyscale"
|
||||||
tooltip={label}
|
size="sm"
|
||||||
aria-label={label}
|
tooltip={label}
|
||||||
isDisabled={isMuted || !canMute}
|
aria-label={label}
|
||||||
onPress={async () =>
|
isDisabled={isMuted || !canMute}
|
||||||
!isMuted && isLocal(participant)
|
onPress={async () =>
|
||||||
? await (participant as LocalParticipant)?.setMicrophoneEnabled(false)
|
!isMuted && isLocal(participant)
|
||||||
: openMuteDialog(participant)
|
? await (participant as LocalParticipant)?.setMicrophoneEnabled(
|
||||||
}
|
false
|
||||||
data-attr="participants-mute"
|
)
|
||||||
>
|
: setIsAlertOpen(true)
|
||||||
{isMuted ? (
|
}
|
||||||
<RiMicOffFill color={'gray'} aria-hidden={true} />
|
data-attr="participants-mute"
|
||||||
) : (
|
>
|
||||||
<RiMicFill
|
{isMuted ? (
|
||||||
className={css({
|
<RiMicOffFill color={'gray'} aria-hidden={true} />
|
||||||
color: isSpeaking ? 'primaryDark.300' : 'primaryDark.50',
|
) : (
|
||||||
animation: isSpeaking
|
<RiMicFill
|
||||||
? 'pulse_background 800ms infinite'
|
className={css({
|
||||||
: undefined,
|
color: isSpeaking ? 'primaryDark.300' : 'primaryDark.50',
|
||||||
})}
|
animation: isSpeaking
|
||||||
aria-hidden={true}
|
? 'pulse_background 800ms infinite'
|
||||||
/>
|
: undefined,
|
||||||
)}
|
})}
|
||||||
</Button>
|
aria-hidden={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<MuteAlertDialog
|
||||||
|
isOpen={isAlertOpen}
|
||||||
|
onSubmit={() =>
|
||||||
|
muteParticipant(participant).then(() => setIsAlertOpen(false))
|
||||||
|
}
|
||||||
|
onClose={() => setIsAlertOpen(false)}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const StyledContainer = styled('div', {
|
|||||||
backgroundColor: 'primaryDark.100',
|
backgroundColor: 'primaryDark.100',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
translate: '0 3.25rem',
|
transform: 'translateY(3.25rem)',
|
||||||
transition: 'opacity, translate',
|
transition: 'opacity, transform',
|
||||||
transitionDuration: '0.5s',
|
transitionDuration: '0.5s',
|
||||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
@@ -36,7 +36,7 @@ const StyledContainer = styled('div', {
|
|||||||
isVisible: {
|
isVisible: {
|
||||||
true: {
|
true: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
translate: '0 0',
|
transform: 'translateY(0)',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,7 +84,7 @@ export const ReactionButtonsContainer = ({
|
|||||||
shouldBeCenteredWithToggleButton,
|
shouldBeCenteredWithToggleButton,
|
||||||
setShouldBeCenteredWithToggleButton,
|
setShouldBeCenteredWithToggleButton,
|
||||||
] = useState(false)
|
] = useState(false)
|
||||||
const [offsetX, setOffsetX] = useState(0)
|
const [rightOffset, setRightOffset] = useState(0)
|
||||||
|
|
||||||
const updateArrows = useCallback(() => {
|
const updateArrows = useCallback(() => {
|
||||||
const el = scrollRef.current
|
const el = scrollRef.current
|
||||||
@@ -115,7 +115,7 @@ export const ReactionButtonsContainer = ({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!shouldBeCenteredWithToggleButton || isMobile) {
|
if (!shouldBeCenteredWithToggleButton || isMobile) {
|
||||||
setOffsetX(0)
|
setRightOffset(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export const ReactionButtonsContainer = ({
|
|||||||
const containerCenterX = containerRect.left + containerRect.width / 2
|
const containerCenterX = containerRect.left + containerRect.width / 2
|
||||||
const shift = toggleCenterX - containerCenterX
|
const shift = toggleCenterX - containerCenterX
|
||||||
if (Math.abs(shift) < 0.5) return
|
if (Math.abs(shift) < 0.5) return
|
||||||
setOffsetX((prev) => prev + shift)
|
setRightOffset((prev) => prev - shift * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedule = () => {
|
const schedule = () => {
|
||||||
@@ -182,7 +182,7 @@ export const ReactionButtonsContainer = ({
|
|||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
style={
|
style={
|
||||||
shouldBeCenteredWithToggleButton && !isMobile && adjustedCentering
|
shouldBeCenteredWithToggleButton && !isMobile && adjustedCentering
|
||||||
? { transform: `translateX(${offsetX}px)` }
|
? { marginRight: `${rightOffset}px` }
|
||||||
: { margin: '0 15px' }
|
: { margin: '0 15px' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import { useStartRecording, useStopRecording } from '@/features/recording'
|
import { useStartRecording, useStopRecording } from '@/features/recording'
|
||||||
import { recordingStore } from '@/stores/recording'
|
import { recordingStore } from '@/stores/recording'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useMutateRecording = () => {
|
export const useMutateRecording = () => {
|
||||||
const { mutateAsync: startRecording, isPending: isPendingToStart } =
|
const { mutateAsync: startRecording, isPending: isPendingToStart } =
|
||||||
useStartRecording({
|
useStartRecording({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
recordingStore.isErrorDialogOpen = 'start'
|
recordingStore.isErrorDialogOpen = 'start'
|
||||||
captureEvent('error-starting-recording')
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { mutateAsync: stopRecording, isPending: isPendingToStop } =
|
const { mutateAsync: stopRecording, isPending: isPendingToStop } =
|
||||||
useStopRecording({
|
useStopRecording({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
recordingStore.isErrorDialogOpen = 'stop'
|
recordingStore.isErrorDialogOpen = 'stop'
|
||||||
captureEvent('error-stopping-recording')
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import {
|
import {
|
||||||
ConnectionError,
|
|
||||||
ConnectionErrorReason,
|
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
Room,
|
Room,
|
||||||
@@ -27,15 +25,12 @@ import { VideoConference } from '../livekit/prefabs/VideoConference'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
import { LocalUserChoices } from '@/stores/userChoices'
|
import { LocalUserChoices } from '@/stores/userChoices'
|
||||||
import {
|
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||||
captureEvent,
|
|
||||||
captureMediaEvent,
|
|
||||||
reportError,
|
|
||||||
} from '@/features/analytics/telemetry'
|
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { isFireFox } from '@/utils/livekit'
|
import { isFireFox } from '@/utils/livekit'
|
||||||
import { useIsMobile } from '@/utils/useIsMobile'
|
import { useIsMobile } from '@/utils/useIsMobile'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
|
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||||
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
||||||
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
@@ -231,20 +226,9 @@ export const Conference = ({
|
|||||||
backgroundColor: 'primaryDark.50 !important',
|
backgroundColor: 'primaryDark.50 !important',
|
||||||
})}
|
})}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const failure = MediaDeviceFailure.getFailure(e)
|
|
||||||
if (failure && failure !== MediaDeviceFailure.Other) return
|
|
||||||
|
|
||||||
// connect() was aborted by a disconnect() before the join completed
|
|
||||||
if (
|
|
||||||
e instanceof ConnectionError &&
|
|
||||||
e.reason === ConnectionErrorReason.Cancelled
|
|
||||||
) {
|
|
||||||
void captureEvent('connection-cancelled')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reportError('livekit_room_error', e, {
|
reportError('livekit_room_error', e, {
|
||||||
path: 'connect_publish',
|
path: 'connect_publish',
|
||||||
|
failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onConnected={async () => {
|
onConnected={async () => {
|
||||||
@@ -263,8 +247,23 @@ export const Conference = ({
|
|||||||
onDisconnected={(e) => {
|
onDisconnected={(e) => {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
|
pc_publisher: connectionObserverStore.publisher && {
|
||||||
|
...connectionObserverStore.publisher,
|
||||||
|
},
|
||||||
|
pc_subscriber: connectionObserverStore.subscriber && {
|
||||||
|
...connectionObserverStore.subscriber,
|
||||||
|
},
|
||||||
|
pc_publisher_changes_count:
|
||||||
|
connectionObserverStore.publisherChangesCount,
|
||||||
|
pc_subscriber_changes_count:
|
||||||
|
connectionObserverStore.subscriberChangesCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectionObserverStore.publisher = null
|
||||||
|
connectionObserverStore.publisherChangesCount = 0
|
||||||
|
connectionObserverStore.subscriber = null
|
||||||
|
connectionObserverStore.subscriberChangesCount = 0
|
||||||
|
|
||||||
switch (e) {
|
switch (e) {
|
||||||
case DisconnectReason.CLIENT_INITIATED:
|
case DisconnectReason.CLIENT_INITIATED:
|
||||||
navigateTo(
|
navigateTo(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { styled, VStack } from '@/styled-system/jsx'
|
import { styled, VStack } from '@/styled-system/jsx'
|
||||||
import { Button as RACButton } from 'react-aria-components'
|
import { Button as RACButton } from 'react-aria-components'
|
||||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||||
|
import type { CandidateInfo } from '@/stores/connectionObserver'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
import { captureEvent } from '@/features/analytics/telemetry'
|
||||||
|
|
||||||
const Card = styled('div', {
|
const Card = styled('div', {
|
||||||
@@ -239,6 +240,10 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
|
|||||||
|
|
||||||
type RatingMetadata = {
|
type RatingMetadata = {
|
||||||
room_id?: string
|
room_id?: string
|
||||||
|
pc_publisher?: CandidateInfo
|
||||||
|
pc_subscriber?: CandidateInfo
|
||||||
|
pc_publisher_changes_count?: number
|
||||||
|
pc_subscriber_changes_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Rating = ({
|
export const Rating = ({
|
||||||
|
|||||||
@@ -9,16 +9,28 @@ import { useSnapshot } from 'valtio'
|
|||||||
import { DisconnectReason, RoomEvent } from 'livekit-client'
|
import { DisconnectReason, RoomEvent } from 'livekit-client'
|
||||||
|
|
||||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||||
|
|
||||||
import { captureEvent, captureMediaEvent } from '@/features/analytics/telemetry'
|
|
||||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||||
|
|
||||||
|
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||||
|
import { isMobileBrowser } from '@livekit/components-core'
|
||||||
|
import { FeatureFlags } from '@/features/analytics/enums'
|
||||||
|
import { captureEvent, captureMediaEvent } from '@/features/analytics/telemetry'
|
||||||
|
|
||||||
|
const CANDIDATE_POLL_INTERVAL_MS = 5000
|
||||||
|
|
||||||
export const ConnectionObserver = () => {
|
export const ConnectionObserver = () => {
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const connectionStartTimeRef = useRef<number | null>(null)
|
const connectionStartTimeRef = useRef<number | null>(null)
|
||||||
|
|
||||||
const { data } = useConfig()
|
const { data } = useConfig()
|
||||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||||
|
|
||||||
|
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.candidatePolling)
|
||||||
|
const isMobile = isMobileBrowser()
|
||||||
|
|
||||||
|
const isAdvancedConnectionObserverEnabled =
|
||||||
|
!isMobile && isAnalyticsEnabled && featureEnabled
|
||||||
|
|
||||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||||
|
|
||||||
const idleDisconnectModalTimeoutRef = useRef<ReturnType<
|
const idleDisconnectModalTimeoutRef = useRef<ReturnType<
|
||||||
@@ -68,6 +80,100 @@ export const ConnectionObserver = () => {
|
|||||||
userPreferencesSnap.is_idle_disconnect_modal_enabled,
|
userPreferencesSnap.is_idle_disconnect_modal_enabled,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdvancedConnectionObserverEnabled) return
|
||||||
|
if (!room) return
|
||||||
|
|
||||||
|
let interval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const pollCandidate = async (
|
||||||
|
label: 'publisher' | 'subscriber',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
pc?: any
|
||||||
|
) => {
|
||||||
|
if (!pc) return
|
||||||
|
|
||||||
|
let stats: RTCStatsReport
|
||||||
|
try {
|
||||||
|
stats = await pc.getStats()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
stats.forEach((report: any) => {
|
||||||
|
if (
|
||||||
|
report.type === 'candidate-pair' &&
|
||||||
|
report.state === 'succeeded' &&
|
||||||
|
report.nominated
|
||||||
|
) {
|
||||||
|
const remoteCandidate = stats.get(report.remoteCandidateId)
|
||||||
|
if (!remoteCandidate) return
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
type: remoteCandidate.candidateType,
|
||||||
|
address: remoteCandidate.address,
|
||||||
|
protocol: remoteCandidate.protocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = connectionObserverStore[label]
|
||||||
|
|
||||||
|
const hasChanged =
|
||||||
|
current?.type !== next.type ||
|
||||||
|
current?.address !== next.address ||
|
||||||
|
current?.protocol !== next.protocol
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
connectionObserverStore[label] = next
|
||||||
|
const key = `${label}ChangesCount` as const
|
||||||
|
connectionObserverStore[key] =
|
||||||
|
(connectionObserverStore[key] || 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
const publisher = room.engine?.pcManager?.publisher
|
||||||
|
const subscriber = room.engine?.pcManager?.subscriber
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
pollCandidate('publisher', publisher),
|
||||||
|
pollCandidate('subscriber', subscriber),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPolling = async () => {
|
||||||
|
if (interval) return // prevent duplicates
|
||||||
|
|
||||||
|
// Initial snapshot
|
||||||
|
await poll()
|
||||||
|
|
||||||
|
interval = setInterval(poll, CANDIDATE_POLL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (!interval) return
|
||||||
|
clearInterval(interval)
|
||||||
|
interval = null
|
||||||
|
}
|
||||||
|
|
||||||
|
room.on(RoomEvent.Connected, startPolling)
|
||||||
|
room.on(RoomEvent.Reconnected, startPolling)
|
||||||
|
|
||||||
|
room.on(RoomEvent.Reconnecting, stopPolling)
|
||||||
|
room.on(RoomEvent.Disconnected, stopPolling)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopPolling()
|
||||||
|
|
||||||
|
room.off(RoomEvent.Connected, startPolling)
|
||||||
|
room.off(RoomEvent.Reconnected, startPolling)
|
||||||
|
room.off(RoomEvent.Reconnecting, stopPolling)
|
||||||
|
room.off(RoomEvent.Disconnected, stopPolling)
|
||||||
|
}
|
||||||
|
}, [room, isAdvancedConnectionObserverEnabled])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAnalyticsEnabled) return
|
if (!isAnalyticsEnabled) return
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useRef } from 'react'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
|
||||||
import { closeMuteDialog, muteDialogStore } from '@/stores/muteDialog'
|
|
||||||
import { MuteAlertDialog } from './MuteAlertDialog'
|
|
||||||
|
|
||||||
export const MuteAlertDialogProvider = () => {
|
|
||||||
const { participant } = useSnapshot(muteDialogStore)
|
|
||||||
const { muteParticipant } = useMuteParticipant()
|
|
||||||
|
|
||||||
const lastNameRef = useRef('')
|
|
||||||
if (participant) {
|
|
||||||
lastNameRef.current = participant.name || participant.identity
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MuteAlertDialog
|
|
||||||
isOpen={!!participant}
|
|
||||||
name={lastNameRef.current}
|
|
||||||
onClose={closeMuteDialog}
|
|
||||||
onSubmit={() => {
|
|
||||||
const target = muteDialogStore.participant
|
|
||||||
if (!target) return
|
|
||||||
muteParticipant(target).then(closeMuteDialog)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
import { A, Button, Dialog, P } from '@/primitives'
|
import { A, Button, Dialog, P } from '@/primitives'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { getOS, type OS } from '@/utils/os'
|
|
||||||
|
|
||||||
const SCREEN_CAPTURE_SETTINGS_LINKS: Partial<Record<OS, string>> = {
|
|
||||||
macos:
|
|
||||||
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
|
||||||
windows: 'ms-settings:privacy-graphicscaptureprogrammatic',
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo - refactor it into a generic system
|
// todo - refactor it into a generic system
|
||||||
export const ScreenShareErrorModal = ({
|
export const ScreenShareErrorModal = ({
|
||||||
@@ -18,8 +11,7 @@ export const ScreenShareErrorModal = ({
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' })
|
||||||
const os = getOS()
|
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1
|
||||||
const settingsHref = SCREEN_CAPTURE_SETTINGS_LINKS[os]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -34,16 +26,15 @@ export const ScreenShareErrorModal = ({
|
|||||||
<>
|
<>
|
||||||
<P>
|
<P>
|
||||||
{t('message')}{' '}
|
{t('message')}{' '}
|
||||||
{settingsHref && (
|
{isMac && (
|
||||||
<>
|
<>
|
||||||
{t('settingsInstructions')}{' '}
|
{t('macInstructions')}{' '}
|
||||||
<A
|
<A
|
||||||
href={settingsHref}
|
href="x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
|
||||||
target="_blank"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
aria-label={t(`settingsLabel.${os}`) + '-' + t('newTab')}
|
aria-label={t('macSystemPreferences') + '-' + t('newTab')}
|
||||||
>
|
>
|
||||||
{t(`settingsLabel.${os}`)}
|
{t('macSystemPreferences')}
|
||||||
</A>
|
</A>
|
||||||
.{' '}
|
.{' '}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+5
-3
@@ -44,7 +44,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
|||||||
videoElement?: HTMLVideoElement
|
videoElement?: HTMLVideoElement
|
||||||
videoElementLoaded?: boolean
|
videoElementLoaded?: boolean
|
||||||
|
|
||||||
// Canvas containing the video processing result, of which we extract as stream.
|
// Canvas containg the video processing result, of which we extract as stream.
|
||||||
outputCanvas?: HTMLCanvasElement
|
outputCanvas?: HTMLCanvasElement
|
||||||
outputCanvasCtx?: CanvasRenderingContext2D
|
outputCanvasCtx?: CanvasRenderingContext2D
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
|||||||
segmentationMaskCanvas?: HTMLCanvasElement
|
segmentationMaskCanvas?: HTMLCanvasElement
|
||||||
segmentationMaskCanvasCtx?: CanvasRenderingContext2D
|
segmentationMaskCanvasCtx?: CanvasRenderingContext2D
|
||||||
|
|
||||||
// Mask containing the inference result.
|
// Mask containg the inference result.
|
||||||
segmentationMask?: ImageData
|
segmentationMask?: ImageData
|
||||||
|
|
||||||
// The resized image of the video source.
|
// The resized image of the video source.
|
||||||
@@ -105,7 +105,9 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
|||||||
|
|
||||||
_initVirtualBackgroundImage() {
|
_initVirtualBackgroundImage() {
|
||||||
if (this.options.type !== 'virtual') {
|
if (this.options.type !== 'virtual') {
|
||||||
return
|
throw new Error(
|
||||||
|
'Virtual background is only supported for virtual background'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsUpdate =
|
const needsUpdate =
|
||||||
|
|||||||
+1
-1
@@ -126,7 +126,7 @@ export const OutputSoundTester = ({
|
|||||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||||
<audio
|
<audio
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
src="/sounds/uprise.mp3"
|
src="sounds/uprise.mp3"
|
||||||
onEnded={() => setIsPlaying(false)}
|
onEnded={() => setIsPlaying(false)}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ import { reportError } from '@/features/analytics/telemetry'
|
|||||||
|
|
||||||
enum BlurRadius {
|
enum BlurRadius {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
LIGHT = 10,
|
LIGHT = 5,
|
||||||
NORMAL = 20,
|
NORMAL = 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSupported = BackgroundProcessorFactory.isSupported()
|
const isSupported = BackgroundProcessorFactory.isSupported()
|
||||||
|
|||||||
@@ -50,16 +50,15 @@ export const useWatchMediaDeviceErrors = (): MediaDeviceAlert & {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDeviceError = (error: Error, kind?: MediaDeviceKind) => {
|
const onDeviceError = (error: Error, kind?: MediaDeviceKind) => {
|
||||||
const failure = MediaDeviceFailure.getFailure(error)
|
const failure = MediaDeviceFailure.getFailure(error)
|
||||||
if (!failure) return
|
if (!failure || !kind) return
|
||||||
if (failure != MediaDeviceFailure.Other) {
|
|
||||||
void captureMediaEvent('media-device-error', {
|
void captureMediaEvent('media-device-error', {
|
||||||
log_code: 'media_devices_error_event',
|
log_code: 'media_devices_error_event',
|
||||||
path: 'connect_publish',
|
path: 'connect_publish',
|
||||||
failure,
|
failure,
|
||||||
kind: kind ?? 'unknown',
|
kind,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
if (!kind) return
|
|
||||||
const permissionKind = PERMISSION_BY_DEVICE_KIND[kind]
|
const permissionKind = PERMISSION_BY_DEVICE_KIND[kind]
|
||||||
switch (failure) {
|
switch (failure) {
|
||||||
case MediaDeviceFailure.DeviceInUse:
|
case MediaDeviceFailure.DeviceInUse:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SubtitlesToggle } from '../../components/controls/SubtitlesToggle'
|
|||||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
||||||
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
||||||
import { MoreOptions } from './MoreOptions'
|
import { MoreOptions } from './MoreOptions'
|
||||||
import { RefObject, useMemo, useState } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||||
import { useFullScreen } from '../../hooks/useFullScreen'
|
import { useFullScreen } from '../../hooks/useFullScreen'
|
||||||
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
|
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
|
||||||
@@ -21,14 +21,7 @@ export function DesktopControlBar({
|
|||||||
onDeviceError,
|
onDeviceError,
|
||||||
}: Readonly<ControlBarAuxProps>) {
|
}: Readonly<ControlBarAuxProps>) {
|
||||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||||
|
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||||
const [controlBarElement, setControlBarElement] =
|
|
||||||
useState<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const desktopControlBarEl = useMemo<RefObject<HTMLDivElement>>(
|
|
||||||
() => ({ current: controlBarElement }),
|
|
||||||
[controlBarElement]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({})
|
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({})
|
||||||
|
|
||||||
@@ -52,7 +45,7 @@ export function DesktopControlBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setControlBarElement}
|
ref={desktopControlBarEl}
|
||||||
className={css({
|
className={css({
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import type { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|||||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const CONTROL_BAR_BREAKPOINT_WIDE = 1100
|
const CONTROL_BAR_BREAKPOINT = 1100
|
||||||
const CONTROL_BAR_BREAKPOINT_NARROW = 1050
|
|
||||||
|
|
||||||
const NavigationControls = ({
|
const NavigationControls = ({
|
||||||
onPress,
|
onPress,
|
||||||
@@ -66,9 +65,10 @@ export const LateralMenu = () => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreakpointObserverProps {
|
interface BreakpointObserverProps {
|
||||||
containerRef: RefObject<HTMLDivElement>
|
containerRef: RefObject<HTMLDivElement>
|
||||||
onWideChange: (isWide: boolean | null) => void
|
onWideChange: (isWide: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreakpointObserver = ({
|
const BreakpointObserver = ({
|
||||||
@@ -76,20 +76,7 @@ const BreakpointObserver = ({
|
|||||||
onWideChange,
|
onWideChange,
|
||||||
}: BreakpointObserverProps) => {
|
}: BreakpointObserverProps) => {
|
||||||
const { width } = useSize(containerRef)
|
const { width } = useSize(containerRef)
|
||||||
const [isWide, setIsWide] = useState<boolean | null>(null)
|
const isWide = width > CONTROL_BAR_BREAKPOINT
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!width) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (width > CONTROL_BAR_BREAKPOINT_WIDE) {
|
|
||||||
setIsWide(true)
|
|
||||||
} else if (width <= CONTROL_BAR_BREAKPOINT_NARROW) {
|
|
||||||
setIsWide(false)
|
|
||||||
} else {
|
|
||||||
setIsWide((prev) => (prev === null ? false : prev))
|
|
||||||
}
|
|
||||||
}, [width])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onWideChange(isWide)
|
onWideChange(isWide)
|
||||||
@@ -103,7 +90,7 @@ export const MoreOptions = ({
|
|||||||
}: {
|
}: {
|
||||||
parentElement: RefObject<HTMLDivElement>
|
parentElement: RefObject<HTMLDivElement>
|
||||||
}) => {
|
}) => {
|
||||||
const [isWide, setIsWide] = useState<boolean | null>(null)
|
const [isWide, setIsWide] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
@@ -120,7 +107,7 @@ export const MoreOptions = ({
|
|||||||
containerRef={parentElement}
|
containerRef={parentElement}
|
||||||
onWideChange={setIsWide}
|
onWideChange={setIsWide}
|
||||||
/>
|
/>
|
||||||
{isWide !== null && (isWide ? <NavigationControls /> : <LateralMenu />)}
|
{isWide ? <NavigationControls /> : <LateralMenu />}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,12 @@ import { SidePanel } from '../components/SidePanel'
|
|||||||
import { RecordingProvider } from '@/features/recording'
|
import { RecordingProvider } from '@/features/recording'
|
||||||
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
||||||
import { ConnectionObserver } from '../components/ConnectionObserver'
|
import { ConnectionObserver } from '../components/ConnectionObserver'
|
||||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
import { reportError } from '@/features/analytics/telemetry'
|
||||||
import { getOS } from '@/utils/os'
|
|
||||||
import { isFireFox } from '@/utils/livekit'
|
|
||||||
import { MediaStateObserver } from '../components/MediaStateObserver'
|
import { MediaStateObserver } from '../components/MediaStateObserver'
|
||||||
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
|
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
|
||||||
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
||||||
import { VideoResolutionSubscription } from '../components/VideoResolutionSubscription'
|
import { VideoResolutionSubscription } from '../components/VideoResolutionSubscription'
|
||||||
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
|
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
|
||||||
import { MuteAlertDialogProvider } from '@/features/rooms/livekit/components/MuteAlertDialogProvider'
|
|
||||||
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
||||||
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
||||||
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
||||||
@@ -39,20 +36,6 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
|
|||||||
SettingsComponent?: React.ComponentType
|
SettingsComponent?: React.ComponentType
|
||||||
}
|
}
|
||||||
|
|
||||||
const getScreenSharePermissionDeniedScope = (
|
|
||||||
error: Error
|
|
||||||
): 'system' | 'user' | 'browser' | null => {
|
|
||||||
if (error.name === 'NotAllowedError') {
|
|
||||||
if (/by system/i.test(error.message)) return 'system'
|
|
||||||
if (/by user/i.test(error.message)) return 'user'
|
|
||||||
return 'browser'
|
|
||||||
}
|
|
||||||
if (error.name === 'NotFoundError' && isFireFox() && getOS() === 'macos') {
|
|
||||||
return 'system'
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application.
|
* The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application.
|
||||||
* It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers
|
* It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers
|
||||||
@@ -78,34 +61,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
|
|
||||||
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
|
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
|
||||||
|
|
||||||
const handleDeviceError = ({
|
|
||||||
source,
|
|
||||||
error,
|
|
||||||
}: {
|
|
||||||
source: Track.Source
|
|
||||||
error: Error
|
|
||||||
}) => {
|
|
||||||
if (source === Track.Source.ScreenShare) {
|
|
||||||
const scope = getScreenSharePermissionDeniedScope(error)
|
|
||||||
if (scope) {
|
|
||||||
if (scope === 'system') setIsShareErrorVisible(true)
|
|
||||||
void captureMediaEvent('screen-share-permission-denied', {
|
|
||||||
at: 'ControlBar.onDeviceError',
|
|
||||||
source,
|
|
||||||
error_name: error.name,
|
|
||||||
error_message: error.message,
|
|
||||||
denied_scope: scope,
|
|
||||||
os: getOS(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reportError('device_switch_failure', error, {
|
|
||||||
at: 'ControlBar.onDeviceError',
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RoomMetadataSynchronizer />
|
<RoomMetadataSynchronizer />
|
||||||
@@ -137,7 +92,21 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
<StageLayout />
|
<StageLayout />
|
||||||
)}
|
)}
|
||||||
</RoomContentArea>
|
</RoomContentArea>
|
||||||
<ControlBar onDeviceError={handleDeviceError} />
|
<ControlBar
|
||||||
|
onDeviceError={(e) => {
|
||||||
|
reportError('device_switch_failure', e.error, {
|
||||||
|
at: 'ControlBar.onDeviceError',
|
||||||
|
source: e.source,
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
e.source == Track.Source.ScreenShare &&
|
||||||
|
e.error.toString() ==
|
||||||
|
'NotAllowedError: Permission denied by system'
|
||||||
|
) {
|
||||||
|
setIsShareErrorVisible(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<SidePanel />
|
<SidePanel />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +114,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
<ConnectionStateToast />
|
<ConnectionStateToast />
|
||||||
<RecordingProvider />
|
<RecordingProvider />
|
||||||
<SettingsDialogProvider />
|
<SettingsDialogProvider />
|
||||||
<MuteAlertDialogProvider />
|
|
||||||
<ReactionPortals />
|
<ReactionPortals />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Rating } from '@/features/rooms/components/Rating.tsx'
|
|||||||
import { useLocation } from 'wouter'
|
import { useLocation } from 'wouter'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { DisconnectReason } from 'livekit-client'
|
import { DisconnectReason } from 'livekit-client'
|
||||||
|
import type { CandidateInfo } from '@/stores/connectionObserver'
|
||||||
|
|
||||||
// fixme - duplicated with home, refactor in a proper style
|
// fixme - duplicated with home, refactor in a proper style
|
||||||
const Heading = styled('h1', {
|
const Heading = styled('h1', {
|
||||||
@@ -47,6 +48,10 @@ const FeedbackRoute = () => {
|
|||||||
const state = window.history.state
|
const state = window.history.state
|
||||||
return {
|
return {
|
||||||
room_id: state?.room_id as string,
|
room_id: state?.room_id as string,
|
||||||
|
pc_publisher: state?.pc_publisher as CandidateInfo,
|
||||||
|
pc_publisher_changes_count: state?.pc_publisher_changes_count as number,
|
||||||
|
pc_subscriber: state?.pc_subscriber as CandidateInfo,
|
||||||
|
pc_subscriber_changes_count: state?.pc_subscriber_changes_count as number,
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -214,11 +214,8 @@
|
|||||||
"title": "Bildschirmfreigabe nicht möglich",
|
"title": "Bildschirmfreigabe nicht möglich",
|
||||||
"ariaLabel": "Bildschirmfreigabe nicht möglich",
|
"ariaLabel": "Bildschirmfreigabe nicht möglich",
|
||||||
"message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.",
|
"message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.",
|
||||||
"settingsInstructions": "Gehe zu den",
|
"macInstructions": "Gehe zu den",
|
||||||
"settingsLabel": {
|
"macSystemPreferences": "Systemeinstellungen",
|
||||||
"macos": "Systemeinstellungen",
|
|
||||||
"windows": "Windows-Datenschutzeinstellungen"
|
|
||||||
},
|
|
||||||
"helpLinkText": "Weitere Informationen findest du unter",
|
"helpLinkText": "Weitere Informationen findest du unter",
|
||||||
"helpLinkLabel": "Bildschirmfreigabeproblem",
|
"helpLinkLabel": "Bildschirmfreigabeproblem",
|
||||||
"closeButton": "Schließen",
|
"closeButton": "Schließen",
|
||||||
|
|||||||
@@ -214,11 +214,8 @@
|
|||||||
"title": "Unable to share your screen",
|
"title": "Unable to share your screen",
|
||||||
"ariaLabel": "Unable to share your screen",
|
"ariaLabel": "Unable to share your screen",
|
||||||
"message": "Your browser may not be allowed to record the screen on your computer.",
|
"message": "Your browser may not be allowed to record the screen on your computer.",
|
||||||
"settingsInstructions": "Go to your",
|
"macInstructions": "Go to your",
|
||||||
"settingsLabel": {
|
"macSystemPreferences": "System Preferences",
|
||||||
"macos": "System Preferences",
|
|
||||||
"windows": "Windows privacy settings"
|
|
||||||
},
|
|
||||||
"helpLinkText": "To learn more, see",
|
"helpLinkText": "To learn more, see",
|
||||||
"helpLinkLabel": "Presentation issue",
|
"helpLinkLabel": "Presentation issue",
|
||||||
"closeButton": "Dismiss",
|
"closeButton": "Dismiss",
|
||||||
|
|||||||
@@ -214,11 +214,8 @@
|
|||||||
"title": "Impossible de partager votre écran",
|
"title": "Impossible de partager votre écran",
|
||||||
"ariaLabel": "Impossible de partager votre écran",
|
"ariaLabel": "Impossible de partager votre écran",
|
||||||
"message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.",
|
"message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.",
|
||||||
"settingsInstructions": "Accédez à vos",
|
"macInstructions": "Accèdez à vos",
|
||||||
"settingsLabel": {
|
"macSystemPreferences": "Préférences système",
|
||||||
"macos": "Préférences système",
|
|
||||||
"windows": "paramètres de confidentialité Windows"
|
|
||||||
},
|
|
||||||
"helpLinkText": "Pour en savoir plus, consulter",
|
"helpLinkText": "Pour en savoir plus, consulter",
|
||||||
"helpLinkLabel": "Problème de présentation",
|
"helpLinkLabel": "Problème de présentation",
|
||||||
"closeButton": "Ignorer",
|
"closeButton": "Ignorer",
|
||||||
|
|||||||
@@ -214,11 +214,8 @@
|
|||||||
"title": "Kan uw scherm niet delen",
|
"title": "Kan uw scherm niet delen",
|
||||||
"ariaLabel": "Kan uw scherm niet delen",
|
"ariaLabel": "Kan uw scherm niet delen",
|
||||||
"message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.",
|
"message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.",
|
||||||
"settingsInstructions": "Ga naar uw",
|
"macInstructions": "Ga naar uw",
|
||||||
"settingsLabel": {
|
"macSystemPreferences": "Systeemvoorkeuren",
|
||||||
"macos": "Systeemvoorkeuren",
|
|
||||||
"windows": "Windows-privacyinstellingen"
|
|
||||||
},
|
|
||||||
"helpLinkText": "Meer informatie, zie",
|
"helpLinkText": "Meer informatie, zie",
|
||||||
"helpLinkLabel": "Presentatieprobleem",
|
"helpLinkLabel": "Presentatieprobleem",
|
||||||
"closeButton": "Negeren",
|
"closeButton": "Negeren",
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
const CHUNK_RELOAD_KEY = 'vite-preload-error-reload-at'
|
||||||
|
const RELOAD_COOLDOWN_MS = 30_000
|
||||||
|
|
||||||
|
window.addEventListener('vite:preloadError', (event) => {
|
||||||
|
try {
|
||||||
|
const lastReloadAt = Number(sessionStorage.getItem(CHUNK_RELOAD_KEY)) || 0
|
||||||
|
if (Date.now() - lastReloadAt <= RELOAD_COOLDOWN_MS) {
|
||||||
|
// Recent reload didn't help: not a stale deploy, surface the error.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(CHUNK_RELOAD_KEY, String(Date.now()))
|
||||||
|
} catch {
|
||||||
|
// Without sessionStorage we cannot guard against a reload loop:
|
||||||
|
// don't auto-reload, let the error propagate.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import { proxy } from 'valtio'
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
export type CandidateInfo = {
|
||||||
|
type: string
|
||||||
|
address: string
|
||||||
|
protocol: string
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
isIdleDisconnectModalOpen: boolean
|
isIdleDisconnectModalOpen: boolean
|
||||||
|
publisher: CandidateInfo | null
|
||||||
|
publisherChangesCount: number
|
||||||
|
subscriber: CandidateInfo | null
|
||||||
|
subscriberChangesCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const connectionObserverStore = proxy<State>({
|
export const connectionObserverStore = proxy<State>({
|
||||||
isIdleDisconnectModalOpen: false,
|
isIdleDisconnectModalOpen: false,
|
||||||
|
publisher: null,
|
||||||
|
publisherChangesCount: 0,
|
||||||
|
subscriber: null,
|
||||||
|
subscriberChangesCount: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { proxy, ref } from 'valtio'
|
|
||||||
import type { Participant } from 'livekit-client'
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
participant: Participant | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const muteDialogStore = proxy<State>({
|
|
||||||
participant: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const openMuteDialog = (participant: Participant) => {
|
|
||||||
muteDialogStore.participant = ref(participant)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const closeMuteDialog = () => {
|
|
||||||
muteDialogStore.participant = null
|
|
||||||
}
|
|
||||||
@@ -5,25 +5,15 @@ import { visualizer } from 'rollup-plugin-visualizer'
|
|||||||
import svgr from 'vite-plugin-svgr'
|
import svgr from 'vite-plugin-svgr'
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
const readPackageJson = (path: string) =>
|
const mediapipeVersion: string = JSON.parse(
|
||||||
JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf-8'))
|
readFileSync(
|
||||||
|
new URL(
|
||||||
const mediapipeVersion: string = readPackageJson(
|
'./node_modules/@mediapipe/tasks-vision/package.json',
|
||||||
'./node_modules/@mediapipe/tasks-vision/package.json'
|
import.meta.url
|
||||||
).version
|
),
|
||||||
|
'utf-8'
|
||||||
const livekitMediapipeVersion: string = readPackageJson(
|
|
||||||
'./node_modules/@livekit/track-processors/package.json'
|
|
||||||
).dependencies['@mediapipe/tasks-vision']
|
|
||||||
|
|
||||||
if (mediapipeVersion !== livekitMediapipeVersion) {
|
|
||||||
throw new Error(
|
|
||||||
`@mediapipe/tasks-vision@${mediapipeVersion} is installed, but ` +
|
|
||||||
`@livekit/track-processors declares "${livekitMediapipeVersion}". ` +
|
|
||||||
`The two must stay in sync: pin "@mediapipe/tasks-vision" to ` +
|
|
||||||
`"${livekitMediapipeVersion}" in package.json.`
|
|
||||||
)
|
)
|
||||||
}
|
).version
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
|
|||||||
Generated
+10
-20
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.1",
|
"@html-to/text-cli": "0.6.0",
|
||||||
"mjml": "5.4.0"
|
"mjml": "5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@html-to/text-cli": {
|
"node_modules/@html-to/text-cli": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@html-to/text-cli/-/text-cli-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@html-to/text-cli/-/text-cli-0.6.0.tgz",
|
||||||
"integrity": "sha512-fnbLS8bra4BkGinXWzUKfalqLPYmz8E2ABT+TgHUZ4inhAUYF2rBEEctOKqZ6FgaJF1iZ//KBBLKVz6nm3RbhA==",
|
"integrity": "sha512-JwlrCBccUM/QkUpc37P+qG+hpkXRbWOVcHd9vM+5D+O82Ak2p8anGRxisr33//aJzlkYNKNg2I8LDh3Bx2tBPg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@selderee/plugin-htmlparser2": "~0.12.0",
|
"@selderee/plugin-htmlparser2": "~0.12.0",
|
||||||
"aspargvs": "~0.7.0",
|
"aspargvs": "~0.7.0",
|
||||||
"deepmerge-ts": "^8.0.1",
|
"deepmerge-ts": "^7.1.5",
|
||||||
"htmlparser2": "^10.1.0",
|
"htmlparser2": "^10.1.0",
|
||||||
"selderee": "~0.12.0"
|
"selderee": "~0.12.0"
|
||||||
},
|
},
|
||||||
@@ -656,19 +656,9 @@
|
|||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge-ts": {
|
"node_modules/deepmerge-ts": {
|
||||||
"version": "8.0.1",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
"integrity": "sha512-szCXE7YLCvLKR9bFPJcvsezOShdalctSvrgN/LM/QGUEPZQajwjmsMObZ6/DuANT5lxzM/wtO8Feubwdkz8myA==",
|
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "ko-fi",
|
|
||||||
"url": "https://ko-fi.com/rebeccastevens"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/deepmerge-ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.1",
|
"@html-to/text-cli": "0.6.0",
|
||||||
"mjml": "5.4.0"
|
"mjml": "5.4.0"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.27.0",
|
"version": "1.26.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.27.0"
|
version = "1.26.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ class Settings(BaseSettings):
|
|||||||
aws_s3_access_key_id: str
|
aws_s3_access_key_id: str
|
||||||
aws_s3_secret_access_key: SecretStr
|
aws_s3_secret_access_key: SecretStr
|
||||||
aws_s3_secure_access: bool = True
|
aws_s3_secure_access: bool = True
|
||||||
aws_s3_region_name: str | None = None
|
|
||||||
aws_transcript_path: str = "transcripts"
|
aws_transcript_path: str = "transcripts"
|
||||||
aws_summary_path: str = "summaries"
|
aws_summary_path: str = "summaries"
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,6 @@ class FileService:
|
|||||||
access_key=settings.aws_s3_access_key_id,
|
access_key=settings.aws_s3_access_key_id,
|
||||||
secret_key=settings.aws_s3_secret_access_key.get_secret_value(),
|
secret_key=settings.aws_s3_secret_access_key.get_secret_value(),
|
||||||
secure=settings.aws_s3_secure_access,
|
secure=settings.aws_s3_secure_access,
|
||||||
region=settings.aws_s3_region_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._bucket_name = settings.aws_storage_bucket_name
|
self._bucket_name = settings.aws_storage_bucket_name
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class RecordingMetadata(BaseModel):
|
|||||||
|
|
||||||
cloud_storage_url: Url = Field(
|
cloud_storage_url: Url = Field(
|
||||||
title="Cloud Storage URL",
|
title="Cloud Storage URL",
|
||||||
description="The URL of the metadata file for speaker assignment.",
|
description="The URL of the metadata file for speaker assignement.",
|
||||||
)
|
)
|
||||||
started_at: AwareDatetime = Field(title="Start time of the recording to transcribe")
|
started_at: AwareDatetime = Field(title="Start time of the recording to transcribe")
|
||||||
ended_at: AwareDatetime = Field(title="End time of the recording to transcribe")
|
ended_at: AwareDatetime = Field(title="End time of the recording to transcribe")
|
||||||
|
|||||||
Reference in New Issue
Block a user