mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 12:19:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d3f6dd8e |
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "18"
|
||||||
|
|
||||||
- name: Restore the mail templates
|
- name: Restore the mail templates
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
|
|||||||
@@ -10,29 +10,6 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(backend) allow searching the recording admin table by owner email
|
|
||||||
- ✨(frontend) add participant color gradient when camera is off #1490
|
|
||||||
- ✨(all) allow forcing SSO display name for authenticated users
|
|
||||||
- ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
|
|
||||||
- ⬆️(mail) update mjml to v5 and @html-to/text-cli
|
|
||||||
- 🚸(frontend) initialize the join input name with the persisted full name
|
|
||||||
- ♻️(frontend) refactor background processors to use the new API
|
|
||||||
- ♻️(frontend) inline model weights to avoid loading them from remote
|
|
||||||
- ♻️(frontend) inline MediaPipe WASM modules to avoid loading from remote
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- 🩹(backend) identify externally provisioned users to PostHog
|
|
||||||
- 🐛(backend) fix info panel crash for unregistered rooms
|
|
||||||
|
|
||||||
## [1.23.0] - 2026-07-08
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- ✨(backend) extend analytics module to support feature flags
|
- ✨(backend) extend analytics module to support feature flags
|
||||||
- ✨(backend) implement feature flags in Posthog analytics backend
|
- ✨(backend) implement feature flags in Posthog analytics backend
|
||||||
- ✨(agents) report errors to Sentry for all LiveKit agents
|
- ✨(agents) report errors to Sentry for all LiveKit agents
|
||||||
@@ -46,12 +23,6 @@ and this project adheres to
|
|||||||
- ♻️(backend) refactor analytics backend from Protocol to abstract class
|
- ♻️(backend) refactor analytics backend from Protocol to abstract class
|
||||||
- 🔥(summary) remove call to summary enabled feature flag
|
- 🔥(summary) remove call to summary enabled feature flag
|
||||||
- ♻️(frontend) wrap MuteEveryoneButton with AdminOrOwnerOnly
|
- ♻️(frontend) wrap MuteEveryoneButton with AdminOrOwnerOnly
|
||||||
- ⬆️(frontend) upgrade livekit-client from 2.19.0 to 2.19.2
|
|
||||||
- ⬆️(frontend) upgrade posthog-js from 1.386.5 to 1.387.0
|
|
||||||
- ⬆️(frontend) upgrade @tanstack/react-query from 5.100.14 to 5.101.0
|
|
||||||
- ⬆️(frontend) update the frontend build image to Node 22
|
|
||||||
- 🔒️(frontend) update docker image to nginx-unprivileged:1.30.3-alpine3.23
|
|
||||||
- ✨(summary) more precise analytics events
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -71,7 +42,6 @@ and this project adheres to
|
|||||||
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks
|
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks
|
||||||
- 🩹(frontend) enable screen share button in PiP #1458
|
- 🩹(frontend) enable screen share button in PiP #1458
|
||||||
- 🐛(backend) support unencoded S3 notification object keys #1455
|
- 🐛(backend) support unencoded S3 notification object keys #1455
|
||||||
- ✨(frontend) prioritize screen share in picture-in-picture layout #1467
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
-12
@@ -15,15 +15,3 @@ the following command inside your docker container:
|
|||||||
(Note : in your development environment, you can `make migrate`.)
|
(Note : in your development environment, you can `make migrate`.)
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## v1.23.0
|
|
||||||
|
|
||||||
As part of the 1.23.0 release, the legacy `api/v1` implementation has been removed from the _experimental_ Summary service and Meet has been migrated to the new `api/v2`.
|
|
||||||
|
|
||||||
**To avoid a breaking change, the Meet backend continues to use the Summary service's v1-compatible API format by default (`SUMMARY_SERVICE_VERSION` setting defaults to `1`).**
|
|
||||||
|
|
||||||
If you are deploying both Meet and Summary from this repository, you must configure the Meet backend to use the v2 API by setting the following environment variable `SUMMARY_SERVICE_VERSION=2`.
|
|
||||||
|
|
||||||
If you are upgrading only the Meet deployment while keeping an older Summary v1 compatible deployment, no action is required, as the v1-compatible API remains the default.
|
|
||||||
|
|
||||||
Note that we plan on removing the legacy `v1` summary compatibility in a future major version. If you have your own implementation for the summary service, we recommend updating its API contract and setting `SUMMARY_SERVICE_VERSION=2`.
|
|
||||||
|
|||||||
@@ -113,12 +113,6 @@ update_python_version "summary"
|
|||||||
# Update agents pyproject.toml
|
# Update agents pyproject.toml
|
||||||
update_python_version "agents"
|
update_python_version "agents"
|
||||||
|
|
||||||
# Run uv lock in agents
|
|
||||||
print_info "Running uv lock in agents..."
|
|
||||||
cd "src/agents"
|
|
||||||
uv lock
|
|
||||||
cd -
|
|
||||||
|
|
||||||
# Update CHANGELOG
|
# Update CHANGELOG
|
||||||
print_info "Updating CHANGELOG..."
|
print_info "Updating CHANGELOG..."
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ---- Front-end image ----
|
# ---- Front-end image ----
|
||||||
FROM node:22-alpine AS frontend-deps
|
FROM node:20-alpine AS frontend-deps
|
||||||
|
|
||||||
WORKDIR /home/frontend/
|
WORKDIR /home/frontend/
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ RUN npx webpack --mode production
|
|||||||
|
|
||||||
|
|
||||||
# ---- Front-end image ----
|
# ---- Front-end image ----
|
||||||
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
|
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.23.0"
|
version = "1.22.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.4",
|
"livekit-agents==1.6.4",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.23.0"
|
version = "1.22.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "livekit-agents" },
|
{ name = "livekit-agents" },
|
||||||
|
|||||||
@@ -402,14 +402,7 @@ class RecordingAdmin(admin.ModelAdmin):
|
|||||||
"""Recording admin interface declaration."""
|
"""Recording admin interface declaration."""
|
||||||
|
|
||||||
inlines = (RecordingAccessInline,)
|
inlines = (RecordingAccessInline,)
|
||||||
search_fields = [
|
search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
|
||||||
"status",
|
|
||||||
"=id",
|
|
||||||
"worker_id",
|
|
||||||
"room__slug",
|
|
||||||
"=room__id",
|
|
||||||
"accesses__user__email",
|
|
||||||
]
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
|
|||||||
@@ -68,9 +68,6 @@ def get_frontend_configuration(request):
|
|||||||
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
|
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
|
||||||
"default_sources": settings.LIVEKIT_DEFAULT_SOURCES,
|
"default_sources": settings.LIVEKIT_DEFAULT_SOURCES,
|
||||||
},
|
},
|
||||||
"authenticated_users_can_edit_display_name": (
|
|
||||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
|
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
|
||||||
return Response(frontend_configuration)
|
return Response(frontend_configuration)
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ from core.services.subtitle import SubtitleException, SubtitleService
|
|||||||
from core.tasks.file import process_file_deletion
|
from core.tasks.file import process_file_deletion
|
||||||
|
|
||||||
from ..authentication.livekit import LiveKitTokenAuthentication
|
from ..authentication.livekit import LiveKitTokenAuthentication
|
||||||
from ..models import RoomAccessLevel
|
|
||||||
from . import permissions, serializers, throttling
|
from . import permissions, serializers, throttling
|
||||||
from .feature_flag import FeatureFlag
|
from .feature_flag import FeatureFlag
|
||||||
|
|
||||||
@@ -268,9 +267,6 @@ class RoomViewSet(
|
|||||||
username = request.query_params.get("username", None)
|
username = request.query_params.get("username", None)
|
||||||
data = {
|
data = {
|
||||||
"id": None,
|
"id": None,
|
||||||
"slug": slug,
|
|
||||||
"is_administrable": False,
|
|
||||||
"access_level": RoomAccessLevel.PUBLIC,
|
|
||||||
"livekit": {
|
"livekit": {
|
||||||
"url": settings.LIVEKIT_CONFIGURATION["url"],
|
"url": settings.LIVEKIT_CONFIGURATION["url"],
|
||||||
"room": slug,
|
"room": slug,
|
||||||
|
|||||||
@@ -215,6 +215,5 @@ class RoomViewSet(
|
|||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
"external_api": True,
|
"external_api": True,
|
||||||
"auth_method": auth_method,
|
"auth_method": auth_method,
|
||||||
"$set": {"email": self.request.user.email},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,9 +130,6 @@ def test_api_rooms_retrieve_anonymous_unregistered_allowed(mock_token):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": None,
|
"id": None,
|
||||||
"slug": "unregistered-room",
|
|
||||||
"access_level": "public",
|
|
||||||
"is_administrable": False,
|
|
||||||
"livekit": {
|
"livekit": {
|
||||||
"url": "test_url_value",
|
"url": "test_url_value",
|
||||||
"room": "unregistered-room",
|
"room": "unregistered-room",
|
||||||
@@ -165,9 +162,6 @@ def test_api_rooms_retrieve_anonymous_unregistered_allowed_not_normalized(mock_t
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": None,
|
"id": None,
|
||||||
"slug": "reunion",
|
|
||||||
"access_level": "public",
|
|
||||||
"is_administrable": False,
|
|
||||||
"livekit": {
|
"livekit": {
|
||||||
"url": "test_url_value",
|
"url": "test_url_value",
|
||||||
"room": "reunion",
|
"room": "reunion",
|
||||||
|
|||||||
@@ -2,109 +2,13 @@
|
|||||||
Test utils functions
|
Test utils functions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=W0621
|
|
||||||
import json
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
|
|
||||||
import jwt
|
|
||||||
import pytest
|
import pytest
|
||||||
from livekit.api import TwirpError
|
from livekit.api import TwirpError
|
||||||
|
|
||||||
from core.factories import UserFactory
|
from core.utils import NotificationError, create_livekit_client, notify_participants
|
||||||
from core.utils import (
|
|
||||||
NotificationError,
|
|
||||||
create_livekit_client,
|
|
||||||
generate_token,
|
|
||||||
notify_participants,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def decode_token(token: str) -> dict:
|
|
||||||
"""Decode a LiveKit JWT access token for inspection."""
|
|
||||||
return jwt.decode(
|
|
||||||
token,
|
|
||||||
settings.LIVEKIT_CONFIGURATION["api_secret"],
|
|
||||||
algorithms=["HS256"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_token_authenticated_uses_full_name():
|
|
||||||
"""The token's display name should default to the user's full name."""
|
|
||||||
user = UserFactory(full_name="Jane Doe")
|
|
||||||
|
|
||||||
token = generate_token(room="my-room", user=user)
|
|
||||||
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Jane Doe"
|
|
||||||
assert claims["sub"] == str(user.sub)
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_token_authenticated_fallback_user_representation():
|
|
||||||
"""
|
|
||||||
When the user has no full name, the token's display name should fall back
|
|
||||||
to the user's string representation.
|
|
||||||
"""
|
|
||||||
user = UserFactory(full_name=None)
|
|
||||||
|
|
||||||
token = generate_token(room="my-room", user=user)
|
|
||||||
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == str(user)
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_token_explicit_username_overrides_default():
|
|
||||||
"""An explicitly provided username should take precedence over the full name."""
|
|
||||||
user = UserFactory(full_name="Jane Doe")
|
|
||||||
|
|
||||||
token = generate_token(room="my-room", user=user, username="Custom Name")
|
|
||||||
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Custom Name"
|
|
||||||
|
|
||||||
|
|
||||||
def test_authenticated_username_ignored_when_editing_disabled(settings):
|
|
||||||
"""With editing disabled, an authenticated user's username is ignored."""
|
|
||||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
|
||||||
user = UserFactory(full_name="Jane Doe")
|
|
||||||
token = generate_token(room="my-room", user=user, username="Custom Name")
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Jane Doe"
|
|
||||||
|
|
||||||
|
|
||||||
def test_authenticated_default_name_unaffected_when_editing_disabled(settings):
|
|
||||||
"""Disabling editing doesn't disturb the default full-name path."""
|
|
||||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
|
||||||
user = UserFactory(full_name="Jane Doe")
|
|
||||||
token = generate_token(room="my-room", user=user)
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Jane Doe"
|
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_uses_username_when_provided():
|
|
||||||
"""An anonymous user's provided username is used as the display name."""
|
|
||||||
token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Guest42"
|
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_username_used_even_when_editing_disabled(settings):
|
|
||||||
"""The setting governs authenticated users only; anonymous can still set a name."""
|
|
||||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
|
||||||
token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Guest42"
|
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_falls_back_to_anonymous_label():
|
|
||||||
"""With no username, an anonymous user is labelled 'Anonymous'."""
|
|
||||||
token = generate_token(room="my-room", user=AnonymousUser())
|
|
||||||
claims = decode_token(token)
|
|
||||||
assert claims["name"] == "Anonymous"
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("asyncio.get_running_loop")
|
@mock.patch("asyncio.get_running_loop")
|
||||||
|
|||||||
@@ -109,16 +109,11 @@ def generate_token(
|
|||||||
default_username = "Anonymous"
|
default_username = "Anonymous"
|
||||||
else:
|
else:
|
||||||
identity = str(user.sub)
|
identity = str(user.sub)
|
||||||
default_username = user.full_name or str(user)
|
default_username = str(user)
|
||||||
|
|
||||||
if color is None:
|
if color is None:
|
||||||
color = generate_color(identity)
|
color = generate_color(identity)
|
||||||
|
|
||||||
can_edit = (
|
|
||||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME or user.is_anonymous
|
|
||||||
)
|
|
||||||
display_name = (username or default_username) if can_edit else default_username
|
|
||||||
|
|
||||||
token = (
|
token = (
|
||||||
AccessToken(
|
AccessToken(
|
||||||
api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
|
api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
|
||||||
@@ -126,7 +121,7 @@ def generate_token(
|
|||||||
)
|
)
|
||||||
.with_grants(video_grants)
|
.with_grants(video_grants)
|
||||||
.with_identity(identity)
|
.with_identity(identity)
|
||||||
.with_name(display_name)
|
.with_name(username or default_username)
|
||||||
.with_attributes(
|
.with_attributes(
|
||||||
{"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
|
{"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
|||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import warnings
|
|
||||||
from os import path
|
from os import path
|
||||||
from socket import gethostbyname, gethostname
|
from socket import gethostbyname, gethostname
|
||||||
|
|
||||||
@@ -671,11 +670,6 @@ class Base(Configuration):
|
|||||||
environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS",
|
environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = values.BooleanValue(
|
|
||||||
True,
|
|
||||||
environ_name="AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME",
|
|
||||||
environ_prefix=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Recording settings
|
# Recording settings
|
||||||
RECORDING_ENABLE = values.BooleanValue(
|
RECORDING_ENABLE = values.BooleanValue(
|
||||||
@@ -1134,20 +1128,6 @@ class Base(Configuration):
|
|||||||
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
cls.SUMMARY_SERVICE_VERSION == 1
|
|
||||||
and cls.SUMMARY_SERVICE_ENDPOINT is not None
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"SUMMARY_SERVICE_VERSION=1 is deprecated. "
|
|
||||||
"The legacy v1 API has been removed from the experimental "
|
|
||||||
"Summary service. Please update your Summary service deployment to "
|
|
||||||
"the v2 API and set SUMMARY_SERVICE_VERSION=2.",
|
|
||||||
# We use UserWarning to make sure it shows up in production deployment
|
|
||||||
UserWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# The SENTRY_DSN setting should be available to activate sentry for an environment
|
# The SENTRY_DSN setting should be available to activate sentry for an environment
|
||||||
if cls.SENTRY_DSN is not None:
|
if cls.SENTRY_DSN is not None:
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.23.0"
|
version = "1.22.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
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.23.0"
|
version = "1.22.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-alpine AS frontend-deps
|
FROM node:20-alpine AS frontend-deps
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ---- Front-end image ----
|
# ---- Front-end image ----
|
||||||
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
|
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
Generated
+20
-135
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.23.0",
|
"version": "1.22.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.23.0",
|
"version": "1.22.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",
|
||||||
@@ -15,11 +15,10 @@
|
|||||||
"@livekit/components-react": "2.9.21",
|
"@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",
|
|
||||||
"@pandacss/preset-panda": "1.11.3",
|
"@pandacss/preset-panda": "1.11.3",
|
||||||
"@react-types/overlays": "3.10.0",
|
"@react-types/overlays": "3.10.0",
|
||||||
"@remixicon/react": "4.9.0",
|
"@remixicon/react": "4.9.0",
|
||||||
"@tanstack/react-query": "5.101.0",
|
"@tanstack/react-query": "5.100.14",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
"derive-valtio": "0.2.0",
|
"derive-valtio": "0.2.0",
|
||||||
@@ -29,8 +28,8 @@
|
|||||||
"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.19.2",
|
"livekit-client": "2.19.0",
|
||||||
"posthog-js": "1.387.0",
|
"posthog-js": "1.386.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-aria": "3.49.0",
|
"react-aria": "3.49.0",
|
||||||
"react-aria-components": "1.18.0",
|
"react-aria-components": "1.18.0",
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "8.0.14",
|
"vite": "8.0.14",
|
||||||
"vite-plugin-static-copy": "4.1.1",
|
|
||||||
"vite-plugin-svgr": "5.2.0"
|
"vite-plugin-svgr": "5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2359,9 +2357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.101.0",
|
"version": "5.100.14",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
|
||||||
"integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
|
"integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2380,12 +2378,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query": {
|
"node_modules/@tanstack/react-query": {
|
||||||
"version": "5.101.0",
|
"version": "5.100.14",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
|
||||||
"integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
|
"integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.101.0"
|
"@tanstack/query-core": "5.100.14"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3194,19 +3192,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/binary-extensions": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
|
||||||
@@ -7109,19 +7094,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-binary-path": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"binary-extensions": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-boolean-object": {
|
"node_modules/is-boolean-object": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
|
||||||
@@ -8063,9 +8035,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/livekit-client": {
|
"node_modules/livekit-client": {
|
||||||
"version": "2.19.2",
|
"version": "2.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.0.tgz",
|
||||||
"integrity": "sha512-Kvk07QYDWRAbmYNLRll04ZIuxMQobW/oLPYnmR1kCy8GGHpU0gqyHf704Rz+29zfy8IJZRjKqeVbzGSKn9sumw==",
|
"integrity": "sha512-aolY1XDAtx0nHKBNm29W9OhzBnSz1CP5kq3phvRhFfi1NbvMXs8tcACjAkZTnIKgihkp+BiJScZZ3tZv0Gz8sA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/mutex": "1.1.1",
|
"@livekit/mutex": "1.1.1",
|
||||||
@@ -8703,19 +8675,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-map": {
|
|
||||||
"version": "7.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz",
|
|
||||||
"integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/package-manager-detector": {
|
"node_modules/package-manager-detector": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
|
||||||
@@ -9074,13 +9033,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/posthog-js": {
|
"node_modules/posthog-js": {
|
||||||
"version": "1.387.0",
|
"version": "1.386.5",
|
||||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.387.0.tgz",
|
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.386.5.tgz",
|
||||||
"integrity": "sha512-Pv1jUMySMN62zoAxdJBJPV8n62lkHdjuWhpeU7izczc5Dqbx3hhqO2hkrNTI8Yx1ezmWk2qUHZs03FuOBubdFQ==",
|
"integrity": "sha512-ASejQQf5Xw0XolMwH/KCLZlZtoyLK6VsvORwGagAtfa8/ElIOF76BMQspkDsRTybEI+uzHqRDm2m/na1Dki2mA==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@posthog/core": "^1.33.0",
|
"@posthog/core": "^1.32.3",
|
||||||
"@posthog/types": "^1.387.0",
|
"@posthog/types": "^1.386.3",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"dompurify": "^3.3.2",
|
"dompurify": "^3.3.2",
|
||||||
"fflate": "^0.4.8",
|
"fflate": "^0.4.8",
|
||||||
@@ -11178,80 +11137,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-static-copy": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chokidar": "^3.6.0",
|
|
||||||
"p-map": "^7.0.4",
|
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"tinyglobby": "^0.2.17"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^22.0.0 || >=24.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/sapphi-red"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-plugin-static-copy/node_modules/chokidar": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"anymatch": "~3.1.2",
|
|
||||||
"braces": "~3.0.2",
|
|
||||||
"glob-parent": "~5.1.2",
|
|
||||||
"is-binary-path": "~2.1.0",
|
|
||||||
"is-glob": "~4.0.1",
|
|
||||||
"normalize-path": "~3.0.0",
|
|
||||||
"readdirp": "~3.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8.10.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-plugin-static-copy/node_modules/glob-parent": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"is-glob": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-plugin-static-copy/node_modules/readdirp": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"picomatch": "^2.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-plugin-svgr": {
|
"node_modules/vite-plugin-svgr": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.23.0",
|
"version": "1.22.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
@@ -17,16 +17,15 @@
|
|||||||
"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",
|
|
||||||
"@libreaudio/la-call": "0.1.4",
|
"@libreaudio/la-call": "0.1.4",
|
||||||
|
"@fontsource/opendyslexic": "5.2.5",
|
||||||
"@livekit/components-react": "2.9.21",
|
"@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",
|
|
||||||
"@pandacss/preset-panda": "1.11.3",
|
"@pandacss/preset-panda": "1.11.3",
|
||||||
"@react-types/overlays": "3.10.0",
|
"@react-types/overlays": "3.10.0",
|
||||||
"@remixicon/react": "4.9.0",
|
"@remixicon/react": "4.9.0",
|
||||||
"@tanstack/react-query": "5.101.0",
|
"@tanstack/react-query": "5.100.14",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
"derive-valtio": "0.2.0",
|
"derive-valtio": "0.2.0",
|
||||||
@@ -36,8 +35,8 @@
|
|||||||
"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.19.2",
|
"livekit-client": "2.19.0",
|
||||||
"posthog-js": "1.387.0",
|
"posthog-js": "1.386.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-aria": "3.49.0",
|
"react-aria": "3.49.0",
|
||||||
"react-aria-components": "1.18.0",
|
"react-aria-components": "1.18.0",
|
||||||
@@ -71,7 +70,6 @@
|
|||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "8.0.14",
|
"vite": "8.0.14",
|
||||||
"vite-plugin-static-copy": "4.1.1",
|
|
||||||
"vite-plugin-svgr": "5.2.0"
|
"vite-plugin-svgr": "5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -58,7 +58,6 @@ export interface ApiConfig {
|
|||||||
transcription_destination?: string
|
transcription_destination?: string
|
||||||
max_participants_for_sound: number
|
max_participants_for_sound: number
|
||||||
auto_mute_on_join_threshold: number
|
auto_mute_on_join_threshold: number
|
||||||
authenticated_users_can_edit_display_name: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchConfig = (): Promise<ApiConfig> => {
|
const fetchConfig = (): Promise<ApiConfig> => {
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||||
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||||
import { useSnapshot } from 'valtio'
|
import { loadUserChoices } from '@livekit/components-core'
|
||||||
import { userStore } from '@/stores/user'
|
|
||||||
|
|
||||||
export const CreateMeetingMenu = () => {
|
export const CreateMeetingMenu = () => {
|
||||||
const { username } = useSnapshot(userStore)
|
const { username } = loadUserChoices()
|
||||||
|
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
const { mutateAsync: createRoom } = useCreateRoom()
|
const { mutateAsync: createRoom } = useCreateRoom()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const PipFocusLayout = memo(
|
|||||||
<ParticipantTile
|
<ParticipantTile
|
||||||
key={getTrackKey(mainTrack)}
|
key={getTrackKey(mainTrack)}
|
||||||
trackRef={mainTrack}
|
trackRef={mainTrack}
|
||||||
disableTileControls
|
|
||||||
/>
|
/>
|
||||||
</MainSlot>
|
</MainSlot>
|
||||||
)}
|
)}
|
||||||
@@ -35,7 +34,6 @@ export const PipFocusLayout = memo(
|
|||||||
<ParticipantTile
|
<ParticipantTile
|
||||||
key={getTrackKey(thumbnailTrack)}
|
key={getTrackKey(thumbnailTrack)}
|
||||||
trackRef={thumbnailTrack}
|
trackRef={thumbnailTrack}
|
||||||
disableTileControls
|
|
||||||
/>
|
/>
|
||||||
</Thumbnail>
|
</Thumbnail>
|
||||||
)}
|
)}
|
||||||
@@ -50,10 +48,9 @@ const FocusContainer = styled('div', {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'primaryDark.100',
|
backgroundColor: 'primaryDark.100',
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,8 +58,6 @@ const MainSlot = styled('div', {
|
|||||||
base: {
|
base: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'& .lk-participant-media-video': {
|
'& .lk-participant-media-video': {
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
},
|
},
|
||||||
@@ -72,13 +67,13 @@ const MainSlot = styled('div', {
|
|||||||
const Thumbnail = styled('div', {
|
const Thumbnail = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '1.25rem',
|
right: '1rem',
|
||||||
bottom: '1.25rem',
|
bottom: '1rem',
|
||||||
width: '42%',
|
width: '42%',
|
||||||
maxWidth: '220px',
|
maxWidth: '220px',
|
||||||
minWidth: '140px',
|
minWidth: '140px',
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: '16 / 9',
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
boxShadow: 'md',
|
boxShadow: 'md',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
|
|||||||
<GridContainer ref={containerRef} style={gridStyle}>
|
<GridContainer ref={containerRef} style={gridStyle}>
|
||||||
{tracks.map((track, index) => (
|
{tracks.map((track, index) => (
|
||||||
<GridCell key={getTrackKey(track)} style={placements[index]}>
|
<GridCell key={getTrackKey(track)} style={placements[index]}>
|
||||||
<ParticipantTile trackRef={track} disableTileControls />
|
<ParticipantTile trackRef={track} />
|
||||||
</GridCell>
|
</GridCell>
|
||||||
))}
|
))}
|
||||||
</GridContainer>
|
</GridContainer>
|
||||||
@@ -54,8 +54,7 @@ const GridContainer = styled('div', {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '0.5rem',
|
gap: '0.25rem',
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ const GridCell = styled('div', {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'primaryDark.100',
|
backgroundColor: 'primaryDark.100',
|
||||||
// Paint on own layer so FLIP transforms don't trigger layout thrash.
|
// Paint on own layer so FLIP transforms don't trigger layout thrash.
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
|
||||||
|
|
||||||
type PipScreenShareLayoutProps = {
|
|
||||||
screenShareTrack: TrackReferenceOrPlaceholder
|
|
||||||
cameraTracks: TrackReferenceOrPlaceholder[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Layout when a screen share is active.
|
|
||||||
* Camera tiles are shown as a compact row at the top; the screen share occupies
|
|
||||||
* the remaining space below, much larger than the camera tiles.
|
|
||||||
*/
|
|
||||||
export const PipScreenShareLayout = memo(
|
|
||||||
({ screenShareTrack, cameraTracks }: PipScreenShareLayoutProps) => {
|
|
||||||
return (
|
|
||||||
<LayoutContainer>
|
|
||||||
{cameraTracks.length > 0 && (
|
|
||||||
<CameraTilesRow>
|
|
||||||
{cameraTracks.map((track) => (
|
|
||||||
<CameraTile key={getTrackKey(track)}>
|
|
||||||
<ParticipantTile trackRef={track} disableTileControls />
|
|
||||||
</CameraTile>
|
|
||||||
))}
|
|
||||||
</CameraTilesRow>
|
|
||||||
)}
|
|
||||||
<ScreenShareSlot>
|
|
||||||
<ParticipantTile trackRef={screenShareTrack} disableTileControls />
|
|
||||||
</ScreenShareSlot>
|
|
||||||
</LayoutContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
PipScreenShareLayout.displayName = 'PipScreenShareLayout'
|
|
||||||
|
|
||||||
const LayoutContainer = styled('div', {
|
|
||||||
base: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '0.5rem',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const CameraTilesRow = styled('div', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
flexShrink: 0,
|
|
||||||
height: '22%',
|
|
||||||
minHeight: '60px',
|
|
||||||
maxHeight: '120px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const CameraTile = styled('div', {
|
|
||||||
base: {
|
|
||||||
position: 'relative',
|
|
||||||
flex: '0 1 auto',
|
|
||||||
height: '100%',
|
|
||||||
aspectRatio: '16 / 9',
|
|
||||||
minWidth: 0,
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'primaryDark.100',
|
|
||||||
'& .lk-participant-tile': {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ScreenShareSlot = styled('div', {
|
|
||||||
base: {
|
|
||||||
position: 'relative',
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'primaryDark.100',
|
|
||||||
'& .lk-participant-tile': {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
'& .lk-participant-media-video': {
|
|
||||||
objectFit: 'contain',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { usePagination, useTracks } from '@livekit/components-react'
|
import { usePagination, useTracks } from '@livekit/components-react'
|
||||||
import { RoomEvent, Track } from 'livekit-client'
|
import { RoomEvent, Track } from 'livekit-client'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { PipFocusLayout } from './PipFocusLayout'
|
import { PipFocusLayout } from './PipFocusLayout'
|
||||||
import { PipGridLayout } from './PipGridLayout'
|
import { PipGridLayout } from './PipGridLayout'
|
||||||
import { PipPagination } from './PipPagination'
|
import { PipPagination } from './PipPagination'
|
||||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
|
||||||
import { StageFrame } from './StageFrame'
|
import { StageFrame } from './StageFrame'
|
||||||
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
||||||
import {
|
import {
|
||||||
@@ -14,12 +13,9 @@ import {
|
|||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PipStage picks between three layouts:
|
* PipStage picks between two layouts based on track count:
|
||||||
* - Screen share mode (any screen share active):
|
* - Focus mode (≤ 2 tracks): one main track + one thumbnail overlay.
|
||||||
* small camera tiles in a row at the top, large screen share below.
|
* - Grid mode (3+ tracks): adaptive tiling.
|
||||||
* - Grid mode (3+ camera tracks, no screen share): adaptive tiling.
|
|
||||||
* - Focus mode (≤ 2 camera tracks, no screen share): one main track
|
|
||||||
* + one thumbnail overlay.
|
|
||||||
*/
|
*/
|
||||||
export const PipStage = () => {
|
export const PipStage = () => {
|
||||||
const tracks = useTracks(
|
const tracks = useTracks(
|
||||||
@@ -45,55 +41,51 @@ export const PipStage = () => {
|
|||||||
[tracks]
|
[tracks]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cap camera tiles in screen-share mode. Called unconditionally for hook rules.
|
// Grid mode order: screen share leads, then cameras (already ordered by
|
||||||
const paginatedCameraTracks = usePagination(MAX_PIP_TILES - 1, cameraTracks)
|
// active speaker via the `ActiveSpeakersChanged` update above).
|
||||||
|
const gridTracks = useMemo(
|
||||||
|
() =>
|
||||||
|
screenShareTrack ? [screenShareTrack, ...cameraTracks] : cameraTracks,
|
||||||
|
[screenShareTrack, cameraTracks]
|
||||||
|
)
|
||||||
|
|
||||||
// Cap the grid at MAX_PIP_TILES per page for the non-screenshare grid mode.
|
// Cap the grid at MAX_PIP_TILES per page. `usePagination` keeps the visible
|
||||||
const pagination = usePagination(MAX_PIP_TILES, cameraTracks)
|
// page visually stable (active/recent speakers stay put) via its internal
|
||||||
|
// `useVisualStableUpdate`. Called unconditionally to respect hook rules.
|
||||||
|
const pagination = usePagination(MAX_PIP_TILES, gridTracks)
|
||||||
|
|
||||||
if (tracks.length === 0) return null
|
if (tracks.length === 0) return null
|
||||||
|
|
||||||
// Screen share active
|
/**
|
||||||
if (screenShareTrack) {
|
* The focus layout shows one main track with one thumbnail overlay,
|
||||||
// Solo presenter: screen share fills the area, camera as small thumbnail
|
* so it can only fit 2 tracks. Beyond that we switch to the grid.
|
||||||
if (cameraTracks.length <= 1) {
|
*/
|
||||||
return (
|
if (gridTracks.length > 2) {
|
||||||
|
return (
|
||||||
|
<StageWrapper>
|
||||||
<StageFrame>
|
<StageFrame>
|
||||||
<PipFocusLayout
|
<PipGridLayout tracks={pagination.tracks} />
|
||||||
mainTrack={screenShareTrack}
|
|
||||||
thumbnailTrack={cameraTracks[0]}
|
|
||||||
/>
|
|
||||||
</StageFrame>
|
</StageFrame>
|
||||||
)
|
<PipPagination
|
||||||
}
|
totalPageCount={pagination.totalPageCount}
|
||||||
// Multiple cameras: camera row at top, screen share below
|
currentPage={pagination.currentPage}
|
||||||
return (
|
nextPage={pagination.nextPage}
|
||||||
<PaginatedStage pagination={paginatedCameraTracks}>
|
prevPage={pagination.prevPage}
|
||||||
<PipScreenShareLayout
|
|
||||||
screenShareTrack={screenShareTrack}
|
|
||||||
cameraTracks={paginatedCameraTracks.tracks}
|
|
||||||
/>
|
/>
|
||||||
</PaginatedStage>
|
</StageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3+ camera tracks → adaptive grid
|
|
||||||
if (cameraTracks.length > 2) {
|
|
||||||
return (
|
|
||||||
<PaginatedStage pagination={pagination}>
|
|
||||||
<PipGridLayout tracks={pagination.tracks} />
|
|
||||||
</PaginatedStage>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ≤ 2 camera tracks → focus layout (main + optional thumbnail)
|
|
||||||
const localCameraTrack = cameraTracks.find(
|
const localCameraTrack = cameraTracks.find(
|
||||||
(track) => track.participant?.isLocal
|
(track) => track.participant?.isLocal
|
||||||
)
|
)
|
||||||
|
|
||||||
const remoteCameraTrack = cameraTracks.find(
|
const remoteCameraTrack = cameraTracks.find(
|
||||||
(track) => !track.participant?.isLocal
|
(track) => !track.participant?.isLocal
|
||||||
)
|
)
|
||||||
const mainTrack = remoteCameraTrack ?? localCameraTrack
|
|
||||||
|
const mainTrack = screenShareTrack ?? remoteCameraTrack ?? localCameraTrack
|
||||||
|
|
||||||
const thumbnailTrack =
|
const thumbnailTrack =
|
||||||
mainTrack === localCameraTrack ? undefined : localCameraTrack
|
mainTrack === localCameraTrack ? undefined : localCameraTrack
|
||||||
|
|
||||||
@@ -104,31 +96,6 @@ export const PipStage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationResult = {
|
|
||||||
totalPageCount: number
|
|
||||||
currentPage: number
|
|
||||||
nextPage: () => void
|
|
||||||
prevPage: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaginatedStage = ({
|
|
||||||
pagination,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
pagination: PaginationResult
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => (
|
|
||||||
<StageWrapper>
|
|
||||||
<StageFrame>{children}</StageFrame>
|
|
||||||
<PipPagination
|
|
||||||
totalPageCount={pagination.totalPageCount}
|
|
||||||
currentPage={pagination.currentPage}
|
|
||||||
nextPage={pagination.nextPage}
|
|
||||||
prevPage={pagination.prevPage}
|
|
||||||
/>
|
|
||||||
</StageWrapper>
|
|
||||||
)
|
|
||||||
|
|
||||||
const StageWrapper = styled('div', {
|
const StageWrapper = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const Container = styled('div', {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
padding: '0.5rem',
|
marginLeft: '0.5rem',
|
||||||
boxSizing: 'border-box',
|
marginRight: '0.5rem',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type ApiRoom = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
slug: string
|
slug: string
|
||||||
pin_code?: string
|
pin_code: string
|
||||||
is_administrable: boolean
|
is_administrable: boolean
|
||||||
access_level: ApiAccessLevel
|
access_level: ApiAccessLevel
|
||||||
livekit?: ApiLiveKit
|
livekit?: ApiLiveKit
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { PictureInPictureConference } from '@/features/pip/components/PictureInP
|
|||||||
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||||
import { userStore } from '@/stores/user'
|
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -54,8 +53,6 @@ export const Conference = ({
|
|||||||
userChoices: LocalUserChoices
|
userChoices: LocalUserChoices
|
||||||
}
|
}
|
||||||
|
|
||||||
const { username } = useSnapshot(userStore)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
posthog.capture('visit-room', { slug: roomId })
|
posthog.capture('visit-room', { slug: roomId })
|
||||||
}, [roomId, posthog])
|
}, [roomId, posthog])
|
||||||
@@ -86,10 +83,10 @@ export const Conference = ({
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
fetchRoom({
|
fetchRoom({
|
||||||
roomId: roomId as string,
|
roomId: roomId as string,
|
||||||
username: username,
|
username: userConfig.username,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.statusCode == '404') {
|
if (error.statusCode == '404') {
|
||||||
createRoom({ slug: roomId, username })
|
createRoom({ slug: roomId, username: userConfig.username })
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@@ -42,17 +42,14 @@ import {
|
|||||||
saveAudioInputDeviceId,
|
saveAudioInputDeviceId,
|
||||||
saveAudioInputEnabled,
|
saveAudioInputEnabled,
|
||||||
saveAudioOutputDeviceId,
|
saveAudioOutputDeviceId,
|
||||||
|
saveUsername,
|
||||||
saveVideoInputDeviceId,
|
saveVideoInputDeviceId,
|
||||||
saveVideoInputEnabled,
|
saveVideoInputEnabled,
|
||||||
userChoicesStore,
|
userChoicesStore,
|
||||||
} from '@/stores/userChoices'
|
} from '@/stores/userChoices'
|
||||||
|
|
||||||
import { saveUsername, userStore } from '@/stores/user'
|
|
||||||
|
|
||||||
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
|
||||||
import { useConfig } from '@/api/useConfig'
|
|
||||||
|
|
||||||
const onError = (e: Error) => console.error('ERROR', e)
|
const onError = (e: Error) => console.error('ERROR', e)
|
||||||
|
|
||||||
@@ -117,9 +114,6 @@ export const Join = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const { data: configData } = useConfig()
|
|
||||||
const { isLoggedIn, user } = useUser()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
audioEnabled,
|
audioEnabled,
|
||||||
videoEnabled,
|
videoEnabled,
|
||||||
@@ -127,10 +121,9 @@ export const Join = ({
|
|||||||
audioOutputDeviceId,
|
audioOutputDeviceId,
|
||||||
videoDeviceId,
|
videoDeviceId,
|
||||||
processorConfig,
|
processorConfig,
|
||||||
|
username,
|
||||||
} = useSnapshot(userChoicesStore)
|
} = useSnapshot(userChoicesStore)
|
||||||
|
|
||||||
const { username } = useSnapshot(userStore)
|
|
||||||
|
|
||||||
const initialUserChoices = useRef<LocalUserChoices | null>(null)
|
const initialUserChoices = useRef<LocalUserChoices | null>(null)
|
||||||
|
|
||||||
if (initialUserChoices.current === null) {
|
if (initialUserChoices.current === null) {
|
||||||
@@ -141,6 +134,7 @@ export const Join = ({
|
|||||||
audioOutputDeviceId,
|
audioOutputDeviceId,
|
||||||
videoDeviceId,
|
videoDeviceId,
|
||||||
processorConfig,
|
processorConfig,
|
||||||
|
username,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,23 +448,20 @@ export const Join = ({
|
|||||||
<H lvl={1} margin="sm" centered>
|
<H lvl={1} margin="sm" centered>
|
||||||
{t('heading')}
|
{t('heading')}
|
||||||
</H>
|
</H>
|
||||||
{(!isLoggedIn ||
|
<Field
|
||||||
configData?.authenticated_users_can_edit_display_name) && (
|
type="text"
|
||||||
<Field
|
onChange={saveUsername}
|
||||||
type="text"
|
label={t('usernameLabel')}
|
||||||
onChange={saveUsername}
|
id="input-name"
|
||||||
label={t('usernameLabel')}
|
defaultValue={username}
|
||||||
id="input-name"
|
validate={(value) => !value && t('errors.usernameEmpty')}
|
||||||
defaultValue={username || user?.full_name}
|
wrapperProps={{
|
||||||
validate={(value) => !value && t('errors.usernameEmpty')}
|
noMargin: true,
|
||||||
wrapperProps={{
|
fullWidth: true,
|
||||||
noMargin: true,
|
}}
|
||||||
fullWidth: true,
|
autoComplete="name"
|
||||||
}}
|
maxLength={50}
|
||||||
autoComplete="name"
|
/>
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Participant } from 'livekit-client'
|
|||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { Avatar } from '@/components/Avatar'
|
import { Avatar } from '@/components/Avatar'
|
||||||
import { useIsSpeaking } from '@livekit/components-react'
|
import { useIsSpeaking } from '@livekit/components-react'
|
||||||
import { getParticipantBackgroundGradient } from '@/features/rooms/utils/getParticipantBackgroundGradient'
|
|
||||||
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
||||||
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
@@ -11,6 +10,7 @@ const StyledParticipantPlaceHolder = styled('div', {
|
|||||||
base: {
|
base: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
backgroundColor: 'primaryDark.100',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -26,10 +26,6 @@ export const ParticipantPlaceholder = ({
|
|||||||
}: ParticipantPlaceholderProps) => {
|
}: ParticipantPlaceholderProps) => {
|
||||||
const isSpeaking = useIsSpeaking(participant)
|
const isSpeaking = useIsSpeaking(participant)
|
||||||
const participantColor = getParticipantColor(participant)
|
const participantColor = getParticipantColor(participant)
|
||||||
const backgroundGradient = useMemo(
|
|
||||||
() => getParticipantBackgroundGradient(participantColor),
|
|
||||||
[participantColor]
|
|
||||||
)
|
|
||||||
|
|
||||||
const placeholderEl = useRef<HTMLDivElement>(null)
|
const placeholderEl = useRef<HTMLDivElement>(null)
|
||||||
const { width, height } = useSize(placeholderEl)
|
const { width, height } = useSize(placeholderEl)
|
||||||
@@ -43,13 +39,7 @@ export const ParticipantPlaceholder = ({
|
|||||||
const initialSize = useMemo(() => Math.round(avatarSize * 0.3), [avatarSize])
|
const initialSize = useMemo(() => Math.round(avatarSize * 0.3), [avatarSize])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledParticipantPlaceHolder
|
<StyledParticipantPlaceHolder ref={placeholderEl}>
|
||||||
ref={placeholderEl}
|
|
||||||
style={{
|
|
||||||
backgroundColor: participantColor,
|
|
||||||
backgroundImage: backgroundGradient,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ export function TrackRefContextIfNeeded(
|
|||||||
|
|
||||||
interface ParticipantTileExtendedProps extends ParticipantTileProps {
|
interface ParticipantTileExtendedProps extends ParticipantTileProps {
|
||||||
disableMetadata?: boolean
|
disableMetadata?: boolean
|
||||||
disableTileControls?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParticipantTile: (
|
export const ParticipantTile: (
|
||||||
@@ -67,7 +66,6 @@ export const ParticipantTile: (
|
|||||||
onParticipantClick,
|
onParticipantClick,
|
||||||
disableSpeakingIndicator,
|
disableSpeakingIndicator,
|
||||||
disableMetadata,
|
disableMetadata,
|
||||||
disableTileControls,
|
|
||||||
...htmlProps
|
...htmlProps
|
||||||
}: ParticipantTileExtendedProps,
|
}: ParticipantTileExtendedProps,
|
||||||
ref
|
ref
|
||||||
@@ -233,7 +231,7 @@ export const ParticipantTile: (
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!disableMetadata && !disableTileControls && (
|
{!disableMetadata && (
|
||||||
<ParticipantTileFocus
|
<ParticipantTileFocus
|
||||||
trackRef={trackReference}
|
trackRef={trackReference}
|
||||||
hasKeyboardFocus={hasKeyboardFocus}
|
hasKeyboardFocus={hasKeyboardFocus}
|
||||||
|
|||||||
+5
-4
@@ -13,10 +13,8 @@ import {
|
|||||||
} from './TimerWorker'
|
} from './TimerWorker'
|
||||||
import {
|
import {
|
||||||
BackgroundProcessorInterface,
|
BackgroundProcessorInterface,
|
||||||
SELFIE_SEGMENTER_MODEL_PATH,
|
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
type ProcessorType,
|
type ProcessorType,
|
||||||
MEDIAPIPE_PATH_WASM,
|
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
const PROCESSING_WIDTH = 256
|
const PROCESSING_WIDTH = 256
|
||||||
@@ -155,10 +153,13 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initSegmenter() {
|
async initSegmenter() {
|
||||||
const vision = await FilesetResolver.forVisionTasks(MEDIAPIPE_PATH_WASM)
|
const vision = await FilesetResolver.forVisionTasks(
|
||||||
|
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
|
||||||
|
)
|
||||||
this.imageSegmenter = await ImageSegmenter.createFromOptions(vision, {
|
this.imageSegmenter = await ImageSegmenter.createFromOptions(vision, {
|
||||||
baseOptions: {
|
baseOptions: {
|
||||||
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH,
|
modelAssetPath:
|
||||||
|
'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter_landscape/float16/latest/selfie_segmenter_landscape.tflite',
|
||||||
delegate: 'CPU', // Use CPU for Firefox.
|
delegate: 'CPU', // Use CPU for Firefox.
|
||||||
},
|
},
|
||||||
runningMode: 'VIDEO',
|
runningMode: 'VIDEO',
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ import {
|
|||||||
TIMEOUT_TICK,
|
TIMEOUT_TICK,
|
||||||
timerWorkerScript,
|
timerWorkerScript,
|
||||||
} from './TimerWorker'
|
} from './TimerWorker'
|
||||||
import {
|
import { ProcessorType } from '.'
|
||||||
FACE_LANDMARKS_MODEL_PATH,
|
|
||||||
ProcessorType,
|
|
||||||
MEDIAPIPE_PATH_WASM,
|
|
||||||
} from '.'
|
|
||||||
|
|
||||||
const PROCESSING_WIDTH = 256 * 3
|
const PROCESSING_WIDTH = 256 * 3
|
||||||
const PROCESSING_HEIGHT = 144 * 3
|
const PROCESSING_HEIGHT = 144 * 3
|
||||||
@@ -132,10 +128,13 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initFaceLandmarker() {
|
async initFaceLandmarker() {
|
||||||
const vision = await FilesetResolver.forVisionTasks(MEDIAPIPE_PATH_WASM)
|
const vision = await FilesetResolver.forVisionTasks(
|
||||||
|
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
|
||||||
|
)
|
||||||
this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
|
this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
|
||||||
baseOptions: {
|
baseOptions: {
|
||||||
modelAssetPath: FACE_LANDMARKS_MODEL_PATH,
|
modelAssetPath:
|
||||||
|
'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task',
|
||||||
delegate: 'GPU',
|
delegate: 'GPU',
|
||||||
},
|
},
|
||||||
runningMode: 'VIDEO',
|
runningMode: 'VIDEO',
|
||||||
|
|||||||
+4
-19
@@ -1,14 +1,13 @@
|
|||||||
import type { ProcessorOptions, Track } from 'livekit-client'
|
import type { ProcessorOptions, Track } from 'livekit-client'
|
||||||
import {
|
import {
|
||||||
|
BackgroundBlur,
|
||||||
ProcessorWrapper,
|
ProcessorWrapper,
|
||||||
BackgroundProcessor,
|
VirtualBackground,
|
||||||
} from '@livekit/track-processors'
|
} from '@livekit/track-processors'
|
||||||
import {
|
import {
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
BackgroundProcessorInterface,
|
BackgroundProcessorInterface,
|
||||||
ProcessorType,
|
ProcessorType,
|
||||||
MEDIAPIPE_PATH_WASM,
|
|
||||||
SELFIE_SEGMENTER_MODEL_PATH,
|
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
|
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
|
||||||
@@ -21,24 +20,10 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
|
|||||||
|
|
||||||
if (opts.type === 'virtual') {
|
if (opts.type === 'virtual') {
|
||||||
this.processorType = ProcessorType.VIRTUAL
|
this.processorType = ProcessorType.VIRTUAL
|
||||||
this.processor = BackgroundProcessor({
|
this.processor = VirtualBackground(opts.imagePath)
|
||||||
mode: 'virtual-background',
|
|
||||||
imagePath: opts.imagePath,
|
|
||||||
assetPaths: {
|
|
||||||
tasksVisionFileSet: MEDIAPIPE_PATH_WASM,
|
|
||||||
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (opts.type === 'blur') {
|
} else if (opts.type === 'blur') {
|
||||||
this.processorType = ProcessorType.BLUR
|
this.processorType = ProcessorType.BLUR
|
||||||
this.processor = BackgroundProcessor({
|
this.processor = BackgroundBlur(opts.blurRadius)
|
||||||
mode: 'background-blur',
|
|
||||||
blurRadius: opts.blurRadius,
|
|
||||||
assetPaths: {
|
|
||||||
tasksVisionFileSet: MEDIAPIPE_PATH_WASM,
|
|
||||||
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Must provide either imagePath for virtual background or blurRadius for blur'
|
'Must provide either imagePath for virtual background or blurRadius for blur'
|
||||||
|
|||||||
@@ -4,14 +4,6 @@ import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
|||||||
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
|
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
|
||||||
import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
|
import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
|
||||||
|
|
||||||
export const SELFIE_SEGMENTER_MODEL_PATH =
|
|
||||||
'/assets/mediapipe/models/selfie_segmenter_landscape.tflite'
|
|
||||||
|
|
||||||
export const FACE_LANDMARKS_MODEL_PATH =
|
|
||||||
'/assets/mediapipe/models/face_landmarker.task'
|
|
||||||
|
|
||||||
export const MEDIAPIPE_PATH_WASM = '/assets/mediapipe/wasm'
|
|
||||||
|
|
||||||
export enum ProcessorType {
|
export enum ProcessorType {
|
||||||
BLUR = 'blur',
|
BLUR = 'blur',
|
||||||
VIRTUAL = 'virtual',
|
VIRTUAL = 'virtual',
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Builds a radial gradient from the participant's avatar color:
|
|
||||||
* brighter in the center (where the avatar sits), darker at the edges.
|
|
||||||
* Mixed in oklch so the color stays vivid as it darkens instead of greying out.
|
|
||||||
*/
|
|
||||||
export const getParticipantBackgroundGradient = (color: string): string =>
|
|
||||||
`radial-gradient(circle at 50% 45%,
|
|
||||||
color-mix(in oklch, ${color} 82%, white) 0%,
|
|
||||||
color-mix(in oklch, ${color} 90%, white) 8%,
|
|
||||||
${color} 35%,
|
|
||||||
color-mix(in oklch, ${color} 85%, black) 65%,
|
|
||||||
color-mix(in oklch, ${color} 65%, black) 100%)`
|
|
||||||
@@ -8,9 +8,8 @@ import { HStack } from '@/styled-system/jsx'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
import { useRenameParticipant } from '@/features/rooms/api/renameParticipant'
|
import { useRenameParticipant } from '@/features/rooms/api/renameParticipant'
|
||||||
import { saveUsername } from '@/stores/user'
|
import { saveUsername } from '@/stores/userChoices'
|
||||||
import { logout } from '@/features/auth/utils/logout'
|
import { logout } from '@/features/auth/utils/logout'
|
||||||
import { useConfig } from '@/api/useConfig'
|
|
||||||
|
|
||||||
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
Pick<TabPanelProps, 'id'>
|
Pick<TabPanelProps, 'id'>
|
||||||
@@ -18,7 +17,6 @@ export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
|||||||
export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
||||||
const { t } = useTranslation('settings')
|
const { t } = useTranslation('settings')
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const { data } = useConfig()
|
|
||||||
const { user, isLoggedIn } = useUser()
|
const { user, isLoggedIn } = useUser()
|
||||||
|
|
||||||
const { renameParticipant } = useRenameParticipant()
|
const { renameParticipant } = useRenameParticipant()
|
||||||
@@ -47,17 +45,15 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
|||||||
return (
|
return (
|
||||||
<TabPanel padding={'md'} flex id={id}>
|
<TabPanel padding={'md'} flex id={id}>
|
||||||
<H lvl={2}>{t('account.heading')}</H>
|
<H lvl={2}>{t('account.heading')}</H>
|
||||||
{(!isLoggedIn || data?.authenticated_users_can_edit_display_name) && (
|
<Field
|
||||||
<Field
|
type="text"
|
||||||
type="text"
|
label={t('account.nameLabel')}
|
||||||
label={t('account.nameLabel')}
|
value={name}
|
||||||
value={name}
|
onChange={setName}
|
||||||
onChange={setName}
|
validate={(value) => {
|
||||||
validate={(value) => {
|
return !value ? <p>{t('account.nameError')}</p> : null
|
||||||
return !value ? <p>{t('account.nameError')}</p> : null
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<H lvl={2}>{t('account.authentication')}</H>
|
<H lvl={2}>{t('account.authentication')}</H>
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { proxy, subscribe } from 'valtio'
|
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
|
||||||
username: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserState(): State {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEYS.USER)
|
|
||||||
if (!stored) return DEFAULT_STATE
|
|
||||||
const parsed = JSON.parse(stored)
|
|
||||||
return {
|
|
||||||
...parsed,
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error(
|
|
||||||
'[UserPreferencesStore] Failed to parse stored settings:',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
return DEFAULT_STATE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const userStore = proxy<State>(getUserState())
|
|
||||||
|
|
||||||
subscribe(userStore, () => {
|
|
||||||
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userStore))
|
|
||||||
})
|
|
||||||
|
|
||||||
export const saveUsername = (username: string) => {
|
|
||||||
userStore.username = username
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ import { VideoQuality } from 'livekit-client'
|
|||||||
|
|
||||||
export type VideoResolution = 'h720' | 'h360' | 'h180'
|
export type VideoResolution = 'h720' | 'h360' | 'h180'
|
||||||
|
|
||||||
export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & {
|
export type LocalUserChoices = LocalUserChoicesLK & {
|
||||||
processorConfig?: ProcessorConfig
|
processorConfig?: ProcessorConfig
|
||||||
noiseReductionEnabled?: boolean
|
noiseReductionEnabled?: boolean
|
||||||
audioOutputDeviceId?: string
|
audioOutputDeviceId?: string
|
||||||
@@ -32,13 +32,7 @@ function getUserChoicesState(): LocalUserChoices {
|
|||||||
|
|
||||||
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
||||||
subscribe(userChoicesStore, () => {
|
subscribe(userChoicesStore, () => {
|
||||||
// TEMPORARY: cast needed because our store omits `username`, which we no
|
saveUserChoices(userChoicesStore, false)
|
||||||
// longer persis in this store, while LiveKit's `saveUserChoices` still expects the full
|
|
||||||
// `LocalUserChoices` shape. `username` ends up `undefined` in the saved
|
|
||||||
// object, which `saveUserChoices` tolerates at runtime.
|
|
||||||
// We are migrating away from LiveKit's persistence logic to our own store
|
|
||||||
// handling for more control — this cast can be removed once that lands.
|
|
||||||
saveUserChoices(userChoicesStore as LocalUserChoicesLK, false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// we run some logic on store loading to check if the processor config is still valid
|
// we run some logic on store loading to check if the processor config is still valid
|
||||||
@@ -96,6 +90,10 @@ export const saveVideoSubscribeQuality = (quality: VideoQuality) => {
|
|||||||
userChoicesStore.videoSubscribeQuality = quality
|
userChoicesStore.videoSubscribeQuality = quality
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const saveUsername = (username: string) => {
|
||||||
|
userChoicesStore.username = username
|
||||||
|
}
|
||||||
|
|
||||||
export const saveNoiseReductionEnabled = (enabled: boolean) => {
|
export const saveNoiseReductionEnabled = (enabled: boolean) => {
|
||||||
userChoicesStore.noiseReductionEnabled = enabled
|
userChoicesStore.noiseReductionEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,5 @@
|
|||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
NOTIFICATIONS: 'app_notification_settings',
|
NOTIFICATIONS: 'app_notification_settings',
|
||||||
USER_PREFERENCES: 'app_user_preferences',
|
USER_PREFERENCES: 'app_user_preferences',
|
||||||
USER: 'app_user',
|
|
||||||
ACCESSIBILITY: 'app_accessibility_settings',
|
ACCESSIBILITY: 'app_accessibility_settings',
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { defineConfig, loadEnv } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
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'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
@@ -12,32 +11,18 @@ export default defineConfig(({ mode }) => {
|
|||||||
react(),
|
react(),
|
||||||
svgr({
|
svgr({
|
||||||
svgrOptions: {
|
svgrOptions: {
|
||||||
replaceAttrValues: {
|
replaceAttrValues: { '#000': 'currentColor', '#000000': 'currentColor', '#1f1f1f': 'currentColor' },
|
||||||
'#000': 'currentColor',
|
|
||||||
'#000000': 'currentColor',
|
|
||||||
'#1f1f1f': 'currentColor',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
viteStaticCopy({
|
env.VITE_ANALYZE === 'true' && visualizer({
|
||||||
targets: [
|
open: true,
|
||||||
{
|
filename: 'rollup-plugin-visualizer/stats.html',
|
||||||
src: 'node_modules/@mediapipe/tasks-vision/wasm/*',
|
gzipSize: true,
|
||||||
dest: 'assets/mediapipe/wasm',
|
brotliSize: true,
|
||||||
rename: { stripBase: 4 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
env.VITE_ANALYZE === 'true' &&
|
|
||||||
visualizer({
|
|
||||||
open: true,
|
|
||||||
filename: 'rollup-plugin-visualizer/stats.html',
|
|
||||||
gzipSize: true,
|
|
||||||
brotliSize: true,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
tsconfigPaths: true,
|
tsconfigPaths: true
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
|
sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
|
||||||
|
|||||||
@@ -191,8 +191,6 @@ backend:
|
|||||||
SUMMARY_SERVICE_API_TOKEN: password
|
SUMMARY_SERVICE_API_TOKEN: password
|
||||||
SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password
|
SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password
|
||||||
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
|
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
|
||||||
OIDC_USERINFO_FULLNAME_FIELDS: first_name, last_name
|
|
||||||
OIDC_USERINFO_SHORTNAME_FIELD: first_name
|
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ DIR_MAILS="../backend/core/templates/mail/html/"
|
|||||||
if [ ! -d "${DIR_MAILS}" ]; then
|
if [ ! -d "${DIR_MAILS}" ]; then
|
||||||
mkdir -p "${DIR_MAILS}";
|
mkdir -p "${DIR_MAILS}";
|
||||||
fi
|
fi
|
||||||
mjml mjml/*.mjml -o "${DIR_MAILS}" --config.allowIncludes true;
|
mjml mjml/*.mjml -o "${DIR_MAILS}";
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
<mj-include path="./partial/header.mjml" />
|
<mj-include path="./partial/header.mjml" />
|
||||||
|
|
||||||
<mj-body mj-class="bg--blue-100">
|
<mj-body mj-class="bg--blue-100">
|
||||||
<!--
|
|
||||||
We load django tags here so they appear in the body of the HTML output.
|
|
||||||
This ensures html-to-text also includes them in the plain text template.
|
|
||||||
-->
|
|
||||||
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
|
|
||||||
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
|
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
|
||||||
<mj-section css-class="wrapper-logo">
|
<mj-section css-class="wrapper-logo">
|
||||||
<mj-column>
|
<mj-column>
|
||||||
@@ -64,7 +59,7 @@
|
|||||||
</mj-column>
|
</mj-column>
|
||||||
</mj-section>
|
</mj-section>
|
||||||
</mj-wrapper>
|
</mj-wrapper>
|
||||||
|
|
||||||
<mj-include path="./partial/footer.mjml" />
|
|
||||||
</mj-body>
|
</mj-body>
|
||||||
|
|
||||||
|
<mj-include path="./partial/footer.mjml" />
|
||||||
</mjml>
|
</mjml>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
<mj-head>
|
<mj-head>
|
||||||
<mj-title>{{ title }}</mj-title>
|
<mj-title>{{ title }}</mj-title>
|
||||||
<mj-preview>{{ title }}</mj-preview>
|
<mj-preview>
|
||||||
<mj-font name="Roboto" href="https://fonts.bunny.net/css?family=roboto:400,700,900" />
|
<!--
|
||||||
|
We load django tags here, in this way there are put within the body in html output
|
||||||
|
so the html-to-text command includes it within its output
|
||||||
|
-->
|
||||||
|
{% load i18n static extra_tags %}
|
||||||
|
{{ title }}
|
||||||
|
</mj-preview>
|
||||||
<mj-attributes>
|
<mj-attributes>
|
||||||
|
<mj-font name="Roboto" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700;900&display=swap" />
|
||||||
<mj-all
|
<mj-all
|
||||||
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
|
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
|
||||||
font-size="16px"
|
font-size="16px"
|
||||||
line-height="1.5em"
|
line-height="1.5em"
|
||||||
color="#3A3A3A"
|
color="#3A3A3A"
|
||||||
/>
|
/>
|
||||||
<mj-text font-family="Roboto, sans-serif" />
|
|
||||||
<mj-button font-family="Roboto, sans-serif" />
|
|
||||||
</mj-attributes>
|
</mj-attributes>
|
||||||
<mj-style>
|
<mj-style>
|
||||||
/* Reset */
|
/* Reset */
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
<mj-include path="./partial/header.mjml" />
|
<mj-include path="./partial/header.mjml" />
|
||||||
|
|
||||||
<mj-body mj-class="bg--blue-100">
|
<mj-body mj-class="bg--blue-100">
|
||||||
<!--
|
|
||||||
We load django tags here so they appear in the body of the HTML output.
|
|
||||||
This ensures html-to-text also includes them in the plain text template.
|
|
||||||
-->
|
|
||||||
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
|
|
||||||
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
|
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
|
||||||
<mj-section css-class="wrapper-logo">
|
<mj-section css-class="wrapper-logo">
|
||||||
<mj-column>
|
<mj-column>
|
||||||
|
|||||||
Generated
+932
-1894
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.23.0",
|
"version": "1.22.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.0",
|
"@html-to/text-cli": "0.5.4",
|
||||||
"mjml": "5.4.0"
|
"mjml": "4.18.0"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.23.0",
|
"version": "1.22.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.23.0",
|
"version": "1.22.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.23.0",
|
"version": "1.22.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.23.0"
|
version = "1.22.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
|
|||||||
@@ -59,16 +59,17 @@ async def create_transcribe_task_v2(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
properties = {}
|
|
||||||
if request.user_email:
|
|
||||||
properties["$set"] = {"email": request.user_email}
|
|
||||||
|
|
||||||
# We track the request, this also properly initializes the user in the
|
# We track the request, this also properly initializes the user in the
|
||||||
# analytics system, so that later feature flags work properly
|
# analytics system, so that later feature flags work properly
|
||||||
analytics.capture(
|
analytics.capture(
|
||||||
settings.posthog_transcript_request,
|
settings.posthog_event_request,
|
||||||
request.user_sub,
|
request.user_sub,
|
||||||
properties=properties,
|
properties={
|
||||||
|
"kind": "transcribe",
|
||||||
|
"$set": {
|
||||||
|
"email": request.user_email,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return TranscribeWebhookPendingPayload(job_id=task.id).model_dump()
|
return TranscribeWebhookPendingPayload(job_id=task.id).model_dump()
|
||||||
@@ -92,15 +93,15 @@ async def create_summarize_task_v2(
|
|||||||
|
|
||||||
# We track the request, this also properly initializes the user in the
|
# We track the request, this also properly initializes the user in the
|
||||||
# analytics system, so that later feature flags work properly
|
# analytics system, so that later feature flags work properly
|
||||||
|
|
||||||
properties = {}
|
|
||||||
if request.user_email:
|
|
||||||
properties["$set"] = {"email": request.user_email}
|
|
||||||
|
|
||||||
analytics.capture(
|
analytics.capture(
|
||||||
settings.posthog_summary_request,
|
settings.posthog_event_request,
|
||||||
request.user_sub,
|
request.user_sub,
|
||||||
properties=properties,
|
properties={
|
||||||
|
"kind": "summarize",
|
||||||
|
"$set": {
|
||||||
|
"email": request.user_email,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return SummarizeWebhookPendingPayload(job_id=task.id).model_dump()
|
return SummarizeWebhookPendingPayload(job_id=task.id).model_dump()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from celery.utils.log import get_task_logger
|
|||||||
from posthog import Posthog
|
from posthog import Posthog
|
||||||
|
|
||||||
from summary.core.config import get_settings
|
from summary.core.config import get_settings
|
||||||
from summary.core.models import SummarizeTaskJob, TranscribeTaskJob
|
from summary.core.models import TranscribeTaskJob
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
@@ -109,15 +109,19 @@ class MetadataManager:
|
|||||||
"""Check if task_id exists in tasks metadata cache."""
|
"""Check if task_id exists in tasks metadata cache."""
|
||||||
return self._redis.exists(self._get_redis_key(task_id))
|
return self._redis.exists(self._get_redis_key(task_id))
|
||||||
|
|
||||||
def create(self, task_id: str, task_payload: TranscribeTaskJob | SummarizeTaskJob):
|
def create(self, task_id: str, task_payload: TranscribeTaskJob):
|
||||||
"""Create initial metadata entry for a new task."""
|
"""Create initial metadata entry for a new task."""
|
||||||
if self._is_disabled or self.has_task_id(task_id):
|
if self._is_disabled or self.has_task_id(task_id):
|
||||||
return
|
return
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
parts = urlsplit(task_payload.cloud_storage_url)
|
||||||
|
clean_url = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
|
||||||
initial_metadata = {
|
initial_metadata = {
|
||||||
"start_time": start_time,
|
"start_time": start_time,
|
||||||
|
"asr_model": settings.whisperx_asr_model,
|
||||||
"retries": 0,
|
"retries": 0,
|
||||||
|
"filename": clean_url,
|
||||||
"sub": task_payload.user_sub,
|
"sub": task_payload.user_sub,
|
||||||
# avoid None in redis, it shouldn't happen anyway in prod
|
# avoid None in redis, it shouldn't happen anyway in prod
|
||||||
"email": task_payload.user_email or "",
|
"email": task_payload.user_email or "",
|
||||||
@@ -125,15 +129,6 @@ class MetadataManager:
|
|||||||
"queuing_time": round(start_time - task_payload.received_at.timestamp(), 2),
|
"queuing_time": round(start_time - task_payload.received_at.timestamp(), 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(task_payload, TranscribeTaskJob):
|
|
||||||
parts = urlsplit(task_payload.cloud_storage_url)
|
|
||||||
clean_url = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
|
|
||||||
initial_metadata["source_url"] = clean_url
|
|
||||||
initial_metadata["asr_model"] = settings.whisperx_asr_model
|
|
||||||
elif isinstance(task_payload, SummarizeTaskJob):
|
|
||||||
initial_metadata["content_length"] = len(task_payload.content)
|
|
||||||
initial_metadata["llm_model"] = settings.llm_model
|
|
||||||
|
|
||||||
self._save_metadata(task_id, initial_metadata)
|
self._save_metadata(task_id, initial_metadata)
|
||||||
|
|
||||||
def retry(self, task_id):
|
def retry(self, task_id):
|
||||||
|
|||||||
@@ -542,28 +542,28 @@ def process_audio_transcribe_v2_task(
|
|||||||
call_webhook_v2_task.apply_async(
|
call_webhook_v2_task.apply_async(
|
||||||
args=[success_payload.model_dump(), payload.tenant_id]
|
args=[success_payload.model_dump(), payload.tenant_id]
|
||||||
)
|
)
|
||||||
metadata_manager.capture(job_id, settings.posthog_transcript_success)
|
metadata_manager.capture(job_id, settings.posthog_event_success)
|
||||||
|
|
||||||
return success_payload.model_dump()
|
return success_payload.model_dump()
|
||||||
|
|
||||||
|
|
||||||
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
|
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
|
||||||
def task_started_transcript(task_id=None, task=None, args=None, **kwargs):
|
def task_started(task_id=None, task=None, args=None, **kwargs):
|
||||||
"""Signal handler called before task execution begins."""
|
"""Signal handler called before task execution begins."""
|
||||||
if args:
|
if args:
|
||||||
metadata_manager.create(task_id, TranscribeTaskJob.model_validate(args[0]))
|
metadata_manager.create(task_id, TranscribeTaskJob.model_validate(args[0]))
|
||||||
|
|
||||||
|
|
||||||
@signals.task_retry.connect(sender=process_audio_transcribe_v2_task)
|
@signals.task_retry.connect(sender=process_audio_transcribe_v2_task)
|
||||||
def task_retry_handler_transcript(request=None, reason=None, einfo=None, **kwargs):
|
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
|
||||||
"""Signal handler called when task execution retries."""
|
"""Signal handler called when task execution retries."""
|
||||||
metadata_manager.retry(request.id)
|
metadata_manager.retry(request.id)
|
||||||
|
|
||||||
|
|
||||||
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
|
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
|
||||||
def task_failure_handler_transcript(task_id, exception=None, **kwargs):
|
def task_failure_handler(task_id, exception=None, **kwargs):
|
||||||
"""Signal handler called when task execution fails permanently."""
|
"""Signal handler called when task execution fails permanently."""
|
||||||
metadata_manager.capture(task_id, settings.posthog_transcript_failure)
|
metadata_manager.capture(task_id, settings.posthog_event_failure)
|
||||||
|
|
||||||
|
|
||||||
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
|
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
|
||||||
@@ -648,30 +648,9 @@ def summarize_v2_task(
|
|||||||
call_webhook_v2_task.apply_async(
|
call_webhook_v2_task.apply_async(
|
||||||
args=[success_payload.model_dump(), payload.tenant_id]
|
args=[success_payload.model_dump(), payload.tenant_id]
|
||||||
)
|
)
|
||||||
metadata_manager.capture(job_id, settings.posthog_summary_success)
|
|
||||||
|
|
||||||
return success_payload.model_dump()
|
return success_payload.model_dump()
|
||||||
|
|
||||||
|
|
||||||
@signals.task_prerun.connect(sender=summarize_v2_task)
|
|
||||||
def task_started_summary(task_id=None, task=None, args=None, **kwargs):
|
|
||||||
"""Signal handler called before task execution begins."""
|
|
||||||
if args:
|
|
||||||
metadata_manager.create(task_id, SummarizeTaskJob.model_validate(args[0]))
|
|
||||||
|
|
||||||
|
|
||||||
@signals.task_retry.connect(sender=summarize_v2_task)
|
|
||||||
def task_retry_handler_summary(request=None, reason=None, einfo=None, **kwargs):
|
|
||||||
"""Signal handler called when task execution retries."""
|
|
||||||
metadata_manager.retry(request.id)
|
|
||||||
|
|
||||||
|
|
||||||
@signals.task_failure.connect(sender=summarize_v2_task)
|
|
||||||
def task_failure_handler_summary(task_id, exception=None, **kwargs):
|
|
||||||
"""Signal handler called when task execution fails permanently."""
|
|
||||||
metadata_manager.capture(task_id, settings.posthog_summary_failure)
|
|
||||||
|
|
||||||
|
|
||||||
@signals.task_failure.connect(sender=summarize_v2_task)
|
@signals.task_failure.connect(sender=summarize_v2_task)
|
||||||
def handle_summarize_v2_failed(
|
def handle_summarize_v2_failed(
|
||||||
sender,
|
sender,
|
||||||
|
|||||||
@@ -130,12 +130,9 @@ class Settings(BaseSettings):
|
|||||||
posthog_enabled: bool = False
|
posthog_enabled: bool = False
|
||||||
posthog_api_key: Optional[str] = None
|
posthog_api_key: Optional[str] = None
|
||||||
posthog_api_host: Optional[str] = "https://eu.i.posthog.com"
|
posthog_api_host: Optional[str] = "https://eu.i.posthog.com"
|
||||||
posthog_transcript_request: str = "transcript-request"
|
posthog_event_failure: str = "transcript-failure"
|
||||||
posthog_transcript_failure: str = "transcript-failure"
|
posthog_event_success: str = "transcript-success"
|
||||||
posthog_transcript_success: str = "transcript-success"
|
posthog_event_request: str = "transcript-request"
|
||||||
posthog_summary_request: str = "summary-request"
|
|
||||||
posthog_summary_failure: str = "summary-failure"
|
|
||||||
posthog_summary_success: str = "summary-success"
|
|
||||||
|
|
||||||
# Langfuse (LLM Observability)
|
# Langfuse (LLM Observability)
|
||||||
langfuse_enabled: bool = False
|
langfuse_enabled: bool = False
|
||||||
|
|||||||
Reference in New Issue
Block a user