Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1333e39164 | |||
| 5931c5605a | |||
| 3a205373dd |
@@ -305,6 +305,3 @@ start-tilt: ## start the kubernetes cluster using kind
|
|||||||
tilt up -f ./bin/Tiltfile
|
tilt up -f ./bin/Tiltfile
|
||||||
.PHONY: build-k8s-cluster
|
.PHONY: build-k8s-cluster
|
||||||
|
|
||||||
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
|
|
||||||
DEV_ENV=dev-keycloak tilt up -f ./bin/Tiltfile
|
|
||||||
.PHONY: build-k8s-cluster
|
|
||||||
|
|||||||
@@ -118,8 +118,6 @@ $ make build-k8s-cluster
|
|||||||
Once the Kubernetes cluster is ready, start the application stack locally:
|
Once the Kubernetes cluster is ready, start the application stack locally:
|
||||||
```shell
|
```shell
|
||||||
$ make start-tilt
|
$ make start-tilt
|
||||||
or
|
|
||||||
$ make start-tilt-keycloak # start stack without Pro Connect, use keycloak
|
|
||||||
```
|
```
|
||||||
These commands set up and run your application environment using Tilt for local Kubernetes development.
|
These commands set up and run your application environment using Tilt for local Kubernetes development.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ docker_build(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .'))
|
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e dev template .'))
|
||||||
|
|
||||||
migration = '''
|
migration = '''
|
||||||
set -eu
|
set -eu
|
||||||
|
|||||||
+405
-405
File diff suppressed because it is too large
Load Diff
+1
-1
Submodule secrets updated: 2ba12db71d...f5aea0c251
@@ -130,18 +130,17 @@ class RoomSerializer(serializers.ModelSerializer):
|
|||||||
del output["configuration"]
|
del output["configuration"]
|
||||||
|
|
||||||
if role is not None or instance.is_public:
|
if role is not None or instance.is_public:
|
||||||
room_id = f"{instance.id!s}"
|
slug = f"{instance.id!s}"
|
||||||
username = request.query_params.get("username", None)
|
username = request.query_params.get("username", None)
|
||||||
|
|
||||||
output["livekit"] = {
|
output["livekit"] = {
|
||||||
"url": settings.LIVEKIT_CONFIGURATION["url"],
|
"url": settings.LIVEKIT_CONFIGURATION["url"],
|
||||||
"room": room_id,
|
"room": slug,
|
||||||
"token": utils.generate_token(
|
"token": utils.generate_token(
|
||||||
room=room_id, user=request.user, username=username
|
room=slug, user=request.user, username=username
|
||||||
),
|
),
|
||||||
"passphrase": utils.get_cached_passphrase(room_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output["is_administrable"] = is_admin
|
output["is_administrable"] = is_admin
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ from core.recording.worker.mediator import (
|
|||||||
|
|
||||||
from . import permissions, serializers
|
from . import permissions, serializers
|
||||||
|
|
||||||
from livekit import api as livekit_api
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
# pylint: disable=too-many-ancestors
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@@ -212,10 +210,6 @@ class RoomViewSet(
|
|||||||
Allow unregistered rooms when activated.
|
Allow unregistered rooms when activated.
|
||||||
For unregistered rooms we only return a null id and the livekit room and token.
|
For unregistered rooms we only return a null id and the livekit room and token.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# todo - determine whether encryption is needed store a shared secret in memory or in redis
|
|
||||||
# todo - check if a secret already exists, else create one.
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
except Http404:
|
except Http404:
|
||||||
@@ -349,36 +343,6 @@ class RoomViewSet(
|
|||||||
{"message": f"Recording stopped for room {room.slug}."}
|
{"message": f"Recording stopped for room {room.slug}."}
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(
|
|
||||||
detail=False,
|
|
||||||
methods=["post"],
|
|
||||||
url_path="livekit-webhook",
|
|
||||||
permission_classes=[],
|
|
||||||
authentication_classes=[],
|
|
||||||
)
|
|
||||||
def handle_livekit_webhook(self, request, pk=None): # pylint: disable=unused-argument
|
|
||||||
"""Handle LiveKit webhook events."""
|
|
||||||
auth_token = request.headers.get("Authorization")
|
|
||||||
if not auth_token:
|
|
||||||
return drf_response.Response(
|
|
||||||
{"error": "Missing LiveKit authentication token"},
|
|
||||||
status=drf_status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
|
|
||||||
token_verifier = livekit_api.TokenVerifier()
|
|
||||||
webhook_receiver = livekit_api.WebhookReceiver(token_verifier)
|
|
||||||
|
|
||||||
webhook_data = webhook_receiver.receive(request.body.decode("utf-8"), auth_token)
|
|
||||||
|
|
||||||
# Todo - livekit triggers a webhook for all events, see if we can restrict webhook to a limited number of events.
|
|
||||||
# Todo - handle Egress stopped / aborted events.
|
|
||||||
|
|
||||||
if webhook_data.event == "room_finished":
|
|
||||||
room_id = webhook_data.room.name
|
|
||||||
utils.clear_cache_passphrase(room_id)
|
|
||||||
|
|
||||||
return drf_response.Response({"message": f"Event processed"})
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceAccessListModelMixin:
|
class ResourceAccessListModelMixin:
|
||||||
"""List mixin for resource access API."""
|
"""List mixin for resource access API."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Authentication Backends for the Meet core app."""
|
"""Authentication Backends for the Meet core app."""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -10,11 +10,6 @@ from mozilla_django_oidc.auth import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.services.marketing_service import (
|
|
||||||
ContactCreationError,
|
|
||||||
ContactData,
|
|
||||||
get_marketing_service,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||||
@@ -91,10 +86,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
password="!", # noqa: S106
|
password="!", # noqa: S106
|
||||||
**claims,
|
**claims,
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL:
|
|
||||||
self.signup_to_marketing_email(email)
|
|
||||||
|
|
||||||
elif not user:
|
elif not user:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -105,26 +96,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def signup_to_marketing_email(email):
|
|
||||||
"""Pragmatic approach to newsletter signup during authentication flow.
|
|
||||||
|
|
||||||
Details:
|
|
||||||
1. Uses a very short timeout (1s) to prevent blocking the auth process
|
|
||||||
2. Silently fails if the marketing service is down/slow to prioritize user experience
|
|
||||||
3. Trade-off: May miss some signups but ensures auth flow remains fast
|
|
||||||
|
|
||||||
Note: For a more robust solution, consider using Async task processing (Celery/Django-Q)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
marketing_service = get_marketing_service()
|
|
||||||
contact_data = ContactData(
|
|
||||||
email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]}
|
|
||||||
)
|
|
||||||
marketing_service.create_contact(contact_data, timeout=1)
|
|
||||||
except (ContactCreationError, ImproperlyConfigured, ImportError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_existing_user(self, sub, email):
|
def get_existing_user(self, sub, email):
|
||||||
"""Fetch existing user by sub or email."""
|
"""Fetch existing user by sub or email."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "meet_user"
|
db_table = "meet_user"
|
||||||
ordering = ("-created_at",)
|
|
||||||
verbose_name = _("user")
|
verbose_name = _("user")
|
||||||
verbose_name_plural = _("users")
|
verbose_name_plural = _("users")
|
||||||
|
|
||||||
@@ -305,7 +304,6 @@ class ResourceAccess(BaseModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "meet_resource_access"
|
db_table = "meet_resource_access"
|
||||||
ordering = ("-created_at",)
|
|
||||||
verbose_name = _("Resource access")
|
verbose_name = _("Resource access")
|
||||||
verbose_name_plural = _("Resource accesses")
|
verbose_name_plural = _("Resource accesses")
|
||||||
constraints = [
|
constraints = [
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
"""Marketing service in charge of pushing data for marketing automation."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Dict, List, Optional, Protocol
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
|
|
||||||
import brevo_python
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ContactCreationError(Exception):
|
|
||||||
"""Raised when the contact creation fails."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ContactData:
|
|
||||||
"""Contact data for marketing service integration."""
|
|
||||||
|
|
||||||
email: str
|
|
||||||
attributes: Optional[Dict[str, str]] = None
|
|
||||||
list_ids: Optional[List[int]] = None
|
|
||||||
update_enabled: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class MarketingServiceProtocol(Protocol):
|
|
||||||
"""Interface for marketing automation service integrations."""
|
|
||||||
|
|
||||||
def create_contact(
|
|
||||||
self, contact_data: ContactData, timeout: Optional[int] = None
|
|
||||||
) -> dict:
|
|
||||||
"""Create or update a contact.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
contact_data: Contact information and attributes
|
|
||||||
timeout: API request timeout in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Service response
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ContactCreationError: If contact creation fails
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BrevoMarketingService:
|
|
||||||
"""Brevo marketing automation integration.
|
|
||||||
|
|
||||||
Handles:
|
|
||||||
- Contact management and segmentation
|
|
||||||
- Marketing campaigns and automation
|
|
||||||
- Email communications
|
|
||||||
|
|
||||||
Configuration via Django settings:
|
|
||||||
- BREVO_API_KEY: API authentication
|
|
||||||
- BREVO_API_CONTACT_LIST_IDS: Default contact lists
|
|
||||||
- BREVO_API_CONTACT_ATTRIBUTES: Default contact attributes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize Brevo (ex-sendinblue) marketing service."""
|
|
||||||
|
|
||||||
if not settings.BREVO_API_KEY:
|
|
||||||
raise ImproperlyConfigured("Brevo API key is required")
|
|
||||||
|
|
||||||
configuration = brevo_python.Configuration()
|
|
||||||
configuration.api_key["api-key"] = settings.BREVO_API_KEY
|
|
||||||
|
|
||||||
self._api_client = brevo_python.ApiClient(configuration)
|
|
||||||
|
|
||||||
def create_contact(self, contact_data: ContactData, timeout=None) -> dict:
|
|
||||||
"""Create or update a Brevo contact.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
contact_data: Contact information and attributes
|
|
||||||
timeout: API request timeout in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Brevo API response
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ContactCreationError: If contact creation fails
|
|
||||||
ImproperlyConfigured: If required settings are missing
|
|
||||||
|
|
||||||
Note:
|
|
||||||
Contact attributes must be pre-configured in Brevo.
|
|
||||||
Changes to attributes can impact existing workflows.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not settings.BREVO_API_CONTACT_LIST_IDS:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"Default Brevo List IDs must be configured in settings."
|
|
||||||
)
|
|
||||||
|
|
||||||
contact_api = brevo_python.ContactsApi(self._api_client)
|
|
||||||
|
|
||||||
attributes = {
|
|
||||||
**settings.BREVO_API_CONTACT_ATTRIBUTES,
|
|
||||||
**(contact_data.attributes or {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
list_ids = (contact_data.list_ids or []) + settings.BREVO_API_CONTACT_LIST_IDS
|
|
||||||
|
|
||||||
contact = brevo_python.CreateContact(
|
|
||||||
email=contact_data.email,
|
|
||||||
attributes=attributes,
|
|
||||||
list_ids=list_ids,
|
|
||||||
update_enabled=contact_data.update_enabled,
|
|
||||||
)
|
|
||||||
|
|
||||||
api_configurations = {}
|
|
||||||
|
|
||||||
if timeout is not None:
|
|
||||||
api_configurations["_request_timeout"] = timeout
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = contact_api.create_contact(contact, **api_configurations)
|
|
||||||
except brevo_python.rest.ApiException as err:
|
|
||||||
logger.exception("Failed to create contact in Brevo")
|
|
||||||
raise ContactCreationError("Failed to create contact in Brevo") from err
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
|
||||||
def get_marketing_service() -> MarketingServiceProtocol:
|
|
||||||
"""Return cached instance of configured marketing service."""
|
|
||||||
marketing_service_cls = import_string(settings.MARKETING_SERVICE_CLASS)
|
|
||||||
return marketing_service_cls()
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
"""Unit tests for the Authentication Backends."""
|
"""Unit tests for the Authentication Backends."""
|
||||||
|
|
||||||
from unittest import mock
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
from core.authentication.backends import OIDCAuthenticationBackend
|
from core.authentication.backends import OIDCAuthenticationBackend
|
||||||
from core.factories import UserFactory
|
from core.factories import UserFactory
|
||||||
from core.services import marketing_service
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -415,139 +412,3 @@ def test_update_user_when_no_update_needed(django_assert_num_queries, claims):
|
|||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
|
|
||||||
assert user.email == "john.doe@example.com"
|
assert user.email == "john.doe@example.com"
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(OIDCAuthenticationBackend, "signup_to_marketing_email")
|
|
||||||
def test_marketing_signup_new_user_enabled(mock_signup, monkeypatch, settings):
|
|
||||||
"""Test marketing signup for new user with settings enabled."""
|
|
||||||
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
email = "test@example.com"
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {"sub": "123", "email": email}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
user = klass.get_or_create_user("test-token", None, None)
|
|
||||||
|
|
||||||
assert user.email == email
|
|
||||||
mock_signup.assert_called_once_with(email)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(OIDCAuthenticationBackend, "signup_to_marketing_email")
|
|
||||||
def test_marketing_signup_new_user_disabled(mock_signup, monkeypatch, settings):
|
|
||||||
"""Test no marketing signup for new user with settings disabled."""
|
|
||||||
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = False
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
email = "test@example.com"
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {"sub": "123", "email": email}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
user = klass.get_or_create_user("test-token", None, None)
|
|
||||||
|
|
||||||
assert user.email == email
|
|
||||||
mock_signup.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(OIDCAuthenticationBackend, "signup_to_marketing_email")
|
|
||||||
def test_marketing_signup_new_user_default_disabled(mock_signup, monkeypatch):
|
|
||||||
"""Test no marketing signup for new user with settings by default disabled."""
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
email = "test@example.com"
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {"sub": "123", "email": email}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
user = klass.get_or_create_user("test-token", None, None)
|
|
||||||
|
|
||||||
assert user.email == email
|
|
||||||
mock_signup.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"is_signup_enabled",
|
|
||||||
[True, False],
|
|
||||||
)
|
|
||||||
@mock.patch.object(OIDCAuthenticationBackend, "signup_to_marketing_email")
|
|
||||||
def test_marketing_signup_existing_user(
|
|
||||||
mock_signup, monkeypatch, settings, is_signup_enabled
|
|
||||||
):
|
|
||||||
"""Test no marketing signup for existing user regardless of settings."""
|
|
||||||
|
|
||||||
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = is_signup_enabled
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
db_user = UserFactory(email="test@example.com")
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {"sub": db_user.sub, "email": db_user.email}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
user = klass.get_or_create_user("test-token", None, None)
|
|
||||||
assert user == db_user
|
|
||||||
mock_signup.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.authentication.backends.get_marketing_service")
|
|
||||||
def test_signup_to_marketing_email_success(mock_marketing):
|
|
||||||
"""Test successful marketing signup."""
|
|
||||||
|
|
||||||
email = "test@example.com"
|
|
||||||
|
|
||||||
# Call the method
|
|
||||||
OIDCAuthenticationBackend.signup_to_marketing_email(email)
|
|
||||||
|
|
||||||
# Verify service interaction
|
|
||||||
mock_service = mock_marketing.return_value
|
|
||||||
mock_service.create_contact.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"error",
|
|
||||||
[
|
|
||||||
ImportError,
|
|
||||||
ImproperlyConfigured,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@mock.patch("core.authentication.backends.get_marketing_service")
|
|
||||||
def test_marketing_signup_handles_service_initialization_errors(
|
|
||||||
mock_marketing, error, settings
|
|
||||||
):
|
|
||||||
"""Tests errors that occur when trying to get/initialize the marketing service."""
|
|
||||||
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True
|
|
||||||
|
|
||||||
mock_marketing.side_effect = error
|
|
||||||
|
|
||||||
# Should not raise any exception
|
|
||||||
OIDCAuthenticationBackend.signup_to_marketing_email("test@example.com")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"error",
|
|
||||||
[
|
|
||||||
marketing_service.ContactCreationError,
|
|
||||||
ImproperlyConfigured,
|
|
||||||
ImportError,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@mock.patch("core.authentication.backends.get_marketing_service")
|
|
||||||
def test_marketing_signup_handles_contact_creation_errors(
|
|
||||||
mock_marketing, error, settings
|
|
||||||
):
|
|
||||||
"""Tests errors that occur during the contact creation process."""
|
|
||||||
|
|
||||||
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True
|
|
||||||
mock_marketing.return_value.create_contact.side_effect = error
|
|
||||||
|
|
||||||
# Should not raise any exception
|
|
||||||
OIDCAuthenticationBackend.signup_to_marketing_email("test@example.com")
|
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
"""
|
|
||||||
Test marketing services.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=W0621,W0613
|
|
||||||
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
import brevo_python
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from core.services.marketing_service import (
|
|
||||||
BrevoMarketingService,
|
|
||||||
ContactCreationError,
|
|
||||||
ContactData,
|
|
||||||
get_marketing_service,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_missing_api_key(settings):
|
|
||||||
"""Test initialization with missing API key."""
|
|
||||||
settings.BREVO_API_KEY = None
|
|
||||||
with pytest.raises(ImproperlyConfigured, match="Brevo API key is required"):
|
|
||||||
BrevoMarketingService()
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_contact_missing_list_ids(settings):
|
|
||||||
"""Test contact creation with missing list IDs."""
|
|
||||||
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.BREVO_API_CONTACT_LIST_IDS = None
|
|
||||||
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
|
|
||||||
|
|
||||||
valid_contact_data = ContactData(
|
|
||||||
email="test@example.com",
|
|
||||||
attributes={"first_name": "Test"},
|
|
||||||
list_ids=[1, 2],
|
|
||||||
update_enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
brevo_service = BrevoMarketingService()
|
|
||||||
|
|
||||||
with pytest.raises(
|
|
||||||
ImproperlyConfigured, match="Default Brevo List IDs must be configured"
|
|
||||||
):
|
|
||||||
brevo_service.create_contact(valid_contact_data)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("brevo_python.ContactsApi")
|
|
||||||
def test_create_contact_success(mock_contact_api):
|
|
||||||
"""Test successful contact creation."""
|
|
||||||
|
|
||||||
mock_api = mock_contact_api.return_value
|
|
||||||
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
|
|
||||||
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
|
|
||||||
|
|
||||||
valid_contact_data = ContactData(
|
|
||||||
email="test@example.com",
|
|
||||||
attributes={"first_name": "Test"},
|
|
||||||
list_ids=[1, 2],
|
|
||||||
update_enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
brevo_service = BrevoMarketingService()
|
|
||||||
|
|
||||||
mock_api.create_contact.return_value = {"id": "test-id"}
|
|
||||||
response = brevo_service.create_contact(valid_contact_data)
|
|
||||||
|
|
||||||
assert response == {"id": "test-id"}
|
|
||||||
|
|
||||||
mock_api.create_contact.assert_called_once()
|
|
||||||
contact_arg = mock_api.create_contact.call_args[0][0]
|
|
||||||
assert contact_arg.email == "test@example.com"
|
|
||||||
assert contact_arg.attributes == {
|
|
||||||
**settings.BREVO_API_CONTACT_ATTRIBUTES,
|
|
||||||
**valid_contact_data.attributes,
|
|
||||||
}
|
|
||||||
assert set(contact_arg.list_ids) == {1, 2, 3, 4}
|
|
||||||
assert contact_arg.update_enabled is True
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("brevo_python.ContactsApi")
|
|
||||||
def test_create_contact_with_timeout(mock_contact_api):
|
|
||||||
"""Test contact creation with timeout."""
|
|
||||||
|
|
||||||
mock_api = mock_contact_api.return_value
|
|
||||||
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
|
|
||||||
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
|
|
||||||
|
|
||||||
valid_contact_data = ContactData(
|
|
||||||
email="test@example.com",
|
|
||||||
attributes={"first_name": "Test"},
|
|
||||||
list_ids=[1, 2],
|
|
||||||
update_enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
brevo_service = BrevoMarketingService()
|
|
||||||
brevo_service.create_contact(valid_contact_data, timeout=30)
|
|
||||||
|
|
||||||
mock_api.create_contact.assert_called_once()
|
|
||||||
assert mock_api.create_contact.call_args[1]["_request_timeout"] == 30
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("brevo_python.ContactsApi")
|
|
||||||
def test_create_contact_api_error(mock_contact_api):
|
|
||||||
"""Test contact creation API error handling."""
|
|
||||||
|
|
||||||
mock_api = mock_contact_api.return_value
|
|
||||||
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
|
|
||||||
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
|
|
||||||
|
|
||||||
valid_contact_data = ContactData(
|
|
||||||
email="test@example.com",
|
|
||||||
attributes={"first_name": "Test"},
|
|
||||||
list_ids=[1, 2],
|
|
||||||
update_enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
brevo_service = BrevoMarketingService()
|
|
||||||
|
|
||||||
mock_api.create_contact.side_effect = brevo_python.rest.ApiException()
|
|
||||||
|
|
||||||
with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"):
|
|
||||||
brevo_service.create_contact(valid_contact_data)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def clear_marketing_cache():
|
|
||||||
"""Clear marketing service cache between tests."""
|
|
||||||
get_marketing_service.cache_clear()
|
|
||||||
yield
|
|
||||||
get_marketing_service.cache_clear()
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_marketing_service_caching(clear_marketing_cache):
|
|
||||||
"""Test marketing service caching behavior."""
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.MARKETING_SERVICE_CLASS = (
|
|
||||||
"core.services.marketing_service.BrevoMarketingService"
|
|
||||||
)
|
|
||||||
|
|
||||||
service1 = get_marketing_service()
|
|
||||||
service2 = get_marketing_service()
|
|
||||||
|
|
||||||
assert service1 is service2
|
|
||||||
assert isinstance(service1, BrevoMarketingService)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_marketing_service_invalid_class(clear_marketing_cache):
|
|
||||||
"""Test handling of invalid service class."""
|
|
||||||
settings.MARKETING_SERVICE_CLASS = "invalid.service.path"
|
|
||||||
|
|
||||||
with pytest.raises(ImportError):
|
|
||||||
get_marketing_service()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.marketing_service.import_string")
|
|
||||||
def test_service_instantiation_called_once(mock_import_string, clear_marketing_cache):
|
|
||||||
"""Test service class is instantiated only once."""
|
|
||||||
|
|
||||||
settings.BREVO_API_KEY = "test-api-key"
|
|
||||||
settings.MARKETING_SERVICE_CLASS = (
|
|
||||||
"core.services.marketing_service.BrevoMarketingService"
|
|
||||||
)
|
|
||||||
get_marketing_service.cache_clear()
|
|
||||||
|
|
||||||
mock_service_cls = mock.Mock()
|
|
||||||
mock_service_instance = mock.Mock()
|
|
||||||
mock_service_cls.return_value = mock_service_instance
|
|
||||||
mock_import_string.return_value = mock_service_cls
|
|
||||||
|
|
||||||
service1 = get_marketing_service()
|
|
||||||
service2 = get_marketing_service()
|
|
||||||
|
|
||||||
mock_import_string.assert_called_once_with(settings.MARKETING_SERVICE_CLASS)
|
|
||||||
mock_service_cls.assert_called_once()
|
|
||||||
assert service1 is service2
|
|
||||||
assert service1 is mock_service_instance
|
|
||||||
@@ -66,7 +66,7 @@ def test_api_users_list_query_email():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()["results"]]
|
||||||
assert user_ids == [str(frank.id), str(nicole.id)]
|
assert user_ids == [str(nicole.id), str(frank.id)]
|
||||||
|
|
||||||
|
|
||||||
def test_api_users_retrieve_me_anonymous():
|
def test_api_users_retrieve_me_anonymous():
|
||||||
|
|||||||
@@ -14,47 +14,6 @@ from django.conf import settings
|
|||||||
|
|
||||||
from livekit.api import AccessToken, VideoGrants
|
from livekit.api import AccessToken, VideoGrants
|
||||||
|
|
||||||
import secrets
|
|
||||||
import string
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
|
|
||||||
import base64
|
|
||||||
|
|
||||||
|
|
||||||
def generate_random_passphrase(length=26):
|
|
||||||
"""Generate a random passphrase using letters and digits"""
|
|
||||||
alphabet = string.ascii_letters + string.digits
|
|
||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
||||||
|
|
||||||
|
|
||||||
def build_room_passphrase_key(room_id: str) -> str:
|
|
||||||
"""Build cache key for room passphrase."""
|
|
||||||
return f"room_passphrase:{room_id}"
|
|
||||||
|
|
||||||
def get_cached_passphrase(room_id: str) -> str:
|
|
||||||
"""Get or generate encrypted passphrase for a room.
|
|
||||||
|
|
||||||
Retrieves existing passphrase from cache or generates,
|
|
||||||
encrypts and caches a new one if not found.
|
|
||||||
"""
|
|
||||||
cypher = Fernet(settings.PASSPHRASE_ENCRYPTION_KEY.encode())
|
|
||||||
cache_key = build_room_passphrase_key(room_id)
|
|
||||||
encrypted_passphrase = cache.get(cache_key)
|
|
||||||
|
|
||||||
if encrypted_passphrase is None:
|
|
||||||
passphrase = generate_random_passphrase()
|
|
||||||
encrypted_passphrase = cypher.encrypt(passphrase.encode()).decode()
|
|
||||||
cache.set(cache_key, encrypted_passphrase, timeout=86400) # 24 hours
|
|
||||||
return passphrase
|
|
||||||
|
|
||||||
return cypher.decrypt(encrypted_passphrase.encode()).decode()
|
|
||||||
|
|
||||||
def clear_room_passphrase(room_id: str) -> None:
|
|
||||||
"""Remove room passphrase from cache."""
|
|
||||||
cache.delete(build_room_passphrase_key(room_id))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_color(identity: str) -> str:
|
def generate_color(identity: str) -> str:
|
||||||
"""Generates a consistent HSL color based on a given identity string.
|
"""Generates a consistent HSL color based on a given identity string.
|
||||||
|
|||||||
@@ -457,34 +457,6 @@ class Base(Configuration):
|
|||||||
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
|
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Marketing and communication settings
|
|
||||||
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
|
|
||||||
False,
|
|
||||||
environ_name="SIGNUP_NEW_USERS_TO_NEWSLETTER",
|
|
||||||
environ_prefix=None,
|
|
||||||
help_text=(
|
|
||||||
"When enabled, new users are automatically added to mailing list "
|
|
||||||
"for product updates, marketing communications, and customized emails. "
|
|
||||||
),
|
|
||||||
)
|
|
||||||
MARKETING_SERVICE_CLASS = values.Value(
|
|
||||||
"core.services.marketing_service.BrevoMarketingService",
|
|
||||||
environ_name="MARKETING_SERVICE_CLASS",
|
|
||||||
environ_prefix=None,
|
|
||||||
)
|
|
||||||
BREVO_API_KEY = values.Value(
|
|
||||||
None, environ_name="BREVO_API_KEY", environ_prefix=None
|
|
||||||
)
|
|
||||||
BREVO_API_CONTACT_LIST_IDS = values.ListValue(
|
|
||||||
[],
|
|
||||||
environ_name="BREVO_API_CONTACT_LIST_IDS",
|
|
||||||
environ_prefix=None,
|
|
||||||
converter=lambda x: int(x), # pylint: disable=unnecessary-lambda
|
|
||||||
)
|
|
||||||
BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True})
|
|
||||||
|
|
||||||
PASSPHRASE_ENCRYPTION_KEY = values.Value(environ_name="PASSPHRASE_ENCRYPTION_KEY", environ_prefix=None)
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@property
|
@property
|
||||||
def ENVIRONMENT(self):
|
def ENVIRONMENT(self):
|
||||||
@@ -531,10 +503,8 @@ class Base(Configuration):
|
|||||||
release=get_release(),
|
release=get_release(),
|
||||||
integrations=[DjangoIntegration()],
|
integrations=[DjangoIntegration()],
|
||||||
)
|
)
|
||||||
|
with sentry_sdk.configure_scope() as scope:
|
||||||
# Add the application name to the Sentry scope
|
scope.set_extra("application", "backend")
|
||||||
scope = sentry_sdk.get_global_scope()
|
|
||||||
scope.set_tag("application", "backend")
|
|
||||||
|
|
||||||
|
|
||||||
class Build(Base):
|
class Build(Base):
|
||||||
|
|||||||
+15
-16
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "0.1.12"
|
version = "0.1.10"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -25,21 +25,20 @@ license = { file = "LICENSE" }
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.35.90",
|
"boto3==1.35.68",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.1.0",
|
||||||
"brevo-python==1.1.2",
|
|
||||||
"celery[redis]==5.4.0",
|
"celery[redis]==5.4.0",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
"django-cors-headers==4.6.0",
|
"django-cors-headers==4.6.0",
|
||||||
"django-countries==7.6.1",
|
"django-countries==7.6.1",
|
||||||
"django-parler==2.3",
|
"django-parler==2.3",
|
||||||
"redis==5.2.1",
|
"redis==5.2.0",
|
||||||
"django-redis==5.4.0",
|
"django-redis==5.4.0",
|
||||||
"django-storages[s3]==1.14.4",
|
"django-storages[s3]==1.14.4",
|
||||||
"django-timezone-field>=5.1",
|
"django-timezone-field>=5.1",
|
||||||
"django==5.1.4",
|
"django==5.1.3",
|
||||||
"djangorestframework==3.15.2",
|
"djangorestframework==3.15.2",
|
||||||
"drf_spectacular==0.28.0",
|
"drf_spectacular==0.27.2",
|
||||||
"dockerflow==2024.4.2",
|
"dockerflow==2024.4.2",
|
||||||
"easy_thumbnails==2.10",
|
"easy_thumbnails==2.10",
|
||||||
"factory_boy==3.3.1",
|
"factory_boy==3.3.1",
|
||||||
@@ -49,16 +48,16 @@ dependencies = [
|
|||||||
"markdown==3.7",
|
"markdown==3.7",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.5.0",
|
||||||
"psycopg[binary]==3.2.3",
|
"psycopg[binary]==3.2.3",
|
||||||
"PyJWT==2.10.1",
|
"PyJWT==2.10.0",
|
||||||
"python-frontmatter==1.1.0",
|
"python-frontmatter==1.1.0",
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
"sentry-sdk==2.19.2",
|
"sentry-sdk==2.19.0",
|
||||||
"url-normalize==1.4.3",
|
"url-normalize==1.4.3",
|
||||||
"WeasyPrint>=60.2",
|
"WeasyPrint>=60.2",
|
||||||
"whitenoise==6.8.2",
|
"whitenoise==6.8.2",
|
||||||
"mozilla-django-oidc==4.0.1",
|
"mozilla-django-oidc==4.0.1",
|
||||||
"livekit-api==0.8.1",
|
"livekit-api==0.8.0",
|
||||||
"aiohttp==3.11.11",
|
"aiohttp==3.11.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -70,20 +69,20 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"django-extensions==3.2.3",
|
"django-extensions==3.2.3",
|
||||||
"drf-spectacular-sidecar==2024.12.1",
|
"drf-spectacular-sidecar==2024.11.1",
|
||||||
"freezegun==1.5.1",
|
"freezegun==1.5.1",
|
||||||
"ipdb==0.13.13",
|
"ipdb==0.13.13",
|
||||||
"ipython==8.31.0",
|
"ipython==8.29.0",
|
||||||
"pyfakefs==5.7.3",
|
"pyfakefs==5.7.1",
|
||||||
"pylint-django==2.6.1",
|
"pylint-django==2.6.1",
|
||||||
"pylint==3.3.3",
|
"pylint==3.3.1",
|
||||||
"pytest-cov==6.0.0",
|
"pytest-cov==6.0.0",
|
||||||
"pytest-django==4.9.0",
|
"pytest-django==4.9.0",
|
||||||
"pytest==8.3.4",
|
"pytest==8.3.3",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.6.1",
|
"pytest-xdist==3.6.1",
|
||||||
"responses==0.25.3",
|
"responses==0.25.3",
|
||||||
"ruff==0.8.4",
|
"ruff==0.8.0",
|
||||||
"types-requests==2.32.0.20241016",
|
"types-requests==2.32.0.20241016",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/components-react": "2.6.9",
|
"@livekit/components-react": "2.6.9",
|
||||||
"@livekit/components-styles": "1.1.4",
|
"@livekit/components-styles": "1.1.4",
|
||||||
@@ -7702,9 +7702,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.8",
|
"version": "3.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { css } from '@/styled-system/css'
|
|||||||
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
|
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Text, A } from '@/primitives'
|
import { Text, A } from '@/primitives'
|
||||||
import { GRIST_FORM } from '@/utils/constants'
|
|
||||||
|
const GRIST_FORM =
|
||||||
|
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4'
|
||||||
|
|
||||||
export const FeedbackBanner = () => {
|
export const FeedbackBanner = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ export const MainNotificationToast = () => {
|
|||||||
if (isMobileBrowser()) {
|
if (isMobileBrowser()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
triggerNotificationSound(NotificationType.ParticipantJoined)
|
triggerNotificationSound(NotificationType.Joined)
|
||||||
toastQueue.add(
|
toastQueue.add(
|
||||||
{
|
{
|
||||||
participant,
|
participant,
|
||||||
type: NotificationType.ParticipantJoined,
|
type: NotificationType.Joined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -67,7 +67,7 @@ export const MainNotificationToast = () => {
|
|||||||
const existingToast = toastQueue.visibleToasts.find(
|
const existingToast = toastQueue.visibleToasts.find(
|
||||||
(toast) =>
|
(toast) =>
|
||||||
toast.content.participant === participant &&
|
toast.content.participant === participant &&
|
||||||
toast.content.type === NotificationType.HandRaised
|
toast.content.type === NotificationType.Raised
|
||||||
)
|
)
|
||||||
|
|
||||||
if (existingToast && prevMetadata.raised && !metadata.raised) {
|
if (existingToast && prevMetadata.raised && !metadata.raised) {
|
||||||
@@ -76,11 +76,11 @@ export const MainNotificationToast = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!existingToast && !prevMetadata.raised && metadata.raised) {
|
if (!existingToast && !prevMetadata.raised && metadata.raised) {
|
||||||
triggerNotificationSound(NotificationType.HandRaised)
|
triggerNotificationSound(NotificationType.Raised)
|
||||||
toastQueue.add(
|
toastQueue.add(
|
||||||
{
|
{
|
||||||
participant,
|
participant,
|
||||||
type: NotificationType.HandRaised,
|
type: NotificationType.Raised,
|
||||||
},
|
},
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
ParticipantJoined = 'participantJoined',
|
Joined = 'joined',
|
||||||
HandRaised = 'handRaised',
|
Default = 'default',
|
||||||
// todo - implement message received notification
|
Raised = 'raised',
|
||||||
|
Lowered = 'lowered',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
|
|||||||
return (
|
return (
|
||||||
<div {...regionProps} ref={ref} className="toast-region">
|
<div {...regionProps} ref={ref} className="toast-region">
|
||||||
{state.visibleToasts.map((toast) => {
|
{state.visibleToasts.map((toast) => {
|
||||||
if (toast.content?.type === NotificationType.ParticipantJoined) {
|
if (toast.content?.type === NotificationType.Joined) {
|
||||||
return <ToastJoined key={toast.key} toast={toast} state={state} />
|
return <ToastJoined key={toast.key} toast={toast} state={state} />
|
||||||
}
|
}
|
||||||
if (toast.content?.type === NotificationType.HandRaised) {
|
if (toast.content?.type === NotificationType.Raised) {
|
||||||
return <ToastRaised key={toast.key} toast={toast} state={state} />
|
return <ToastRaised key={toast.key} toast={toast} state={state} />
|
||||||
}
|
}
|
||||||
return <Toast key={toast.key} toast={toast} state={state} />
|
return <Toast key={toast.key} toast={toast} state={state} />
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import useSound from 'use-sound'
|
import useSound from 'use-sound'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { notificationsStore } from '@/stores/notifications'
|
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
|
||||||
|
|
||||||
// fixme - handle dynamic audio output changes
|
// fixme - handle dynamic audio output changes
|
||||||
export const useNotificationSound = () => {
|
export const useNotificationSound = () => {
|
||||||
const notificationsSnap = useSnapshot(notificationsStore)
|
|
||||||
const [play] = useSound('./sounds/notifications.mp3', {
|
const [play] = useSound('./sounds/notifications.mp3', {
|
||||||
sprite: {
|
sprite: {
|
||||||
participantJoined: [0, 1150],
|
joined: [0, 1150],
|
||||||
handRaised: [1400, 180],
|
raised: [1400, 180],
|
||||||
messageReceived: [1580, 300],
|
message: [1580, 300],
|
||||||
waiting: [2039, 710],
|
waiting: [2039, 710],
|
||||||
success: [2740, 1304],
|
success: [2740, 1304],
|
||||||
},
|
},
|
||||||
volume: notificationsSnap.soundNotificationVolume,
|
|
||||||
})
|
})
|
||||||
const triggerNotificationSound = (type: NotificationType) => {
|
const triggerNotificationSound = (type: string) => {
|
||||||
const isSoundEnabled = notificationsSnap.soundNotifications.get(type)
|
play({ id: type })
|
||||||
if (isSoundEnabled) play({ id: type })
|
|
||||||
}
|
}
|
||||||
return { triggerNotificationSound }
|
return { triggerNotificationSound }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export type ApiRoom = {
|
|||||||
url: string
|
url: string
|
||||||
room: string
|
room: string
|
||||||
token: string
|
token: string
|
||||||
passphrase: string
|
|
||||||
}
|
}
|
||||||
configuration?: {
|
configuration?: {
|
||||||
[key: string]: string | number | boolean
|
[key: string]: string | number | boolean
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { LiveKitRoom, type LocalUserChoices } from '@livekit/components-react'
|
import { LiveKitRoom, type LocalUserChoices } from '@livekit/components-react'
|
||||||
import {
|
import { Room, RoomOptions } from 'livekit-client'
|
||||||
Room,
|
|
||||||
RoomOptions,
|
|
||||||
ExternalE2EEKeyProvider,
|
|
||||||
DeviceUnsupportedError,
|
|
||||||
} from 'livekit-client'
|
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
@@ -22,9 +17,6 @@ import { VideoConference } from '../livekit/prefabs/VideoConference'
|
|||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
|
||||||
// todo - release worker when quitting the room, same for the key provider?
|
|
||||||
// todo - check, seems the demo app from livekit trigger the web worker twice because of re-rendering
|
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
userConfig,
|
userConfig,
|
||||||
@@ -71,49 +63,12 @@ export const Conference = ({
|
|||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const e2eeEnabled = true
|
|
||||||
|
|
||||||
const workerRef = useRef<Worker | null>(null)
|
|
||||||
const keyProvider = useRef<any | null>(null)
|
|
||||||
|
|
||||||
const getKeyProvider = () => {
|
|
||||||
if (!keyProvider.current && typeof window !== 'undefined') {
|
|
||||||
keyProvider.current = new ExternalE2EEKeyProvider()
|
|
||||||
}
|
|
||||||
return keyProvider.current
|
|
||||||
}
|
|
||||||
|
|
||||||
const getWorker = () => {
|
|
||||||
if (!e2eeEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!workerRef.current && typeof window !== 'undefined') {
|
|
||||||
workerRef.current = new Worker(
|
|
||||||
new URL('livekit-client/e2ee-worker', import.meta.url)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return workerRef.current
|
|
||||||
}
|
|
||||||
|
|
||||||
const e2eePassphrase = data?.livekit?.passphrase
|
|
||||||
|
|
||||||
const [e2eeSetupComplete, setE2eeSetupComplete] = useState(false)
|
|
||||||
|
|
||||||
const roomOptions = useMemo((): RoomOptions => {
|
const roomOptions = useMemo((): RoomOptions => {
|
||||||
const worker = getWorker()
|
|
||||||
const keyProvider = getKeyProvider()
|
|
||||||
|
|
||||||
// todo - explain why
|
|
||||||
const videoCodec = e2eeEnabled ? undefined : 'vp9'
|
|
||||||
const e2ee = e2eeEnabled ? { keyProvider, worker } : undefined
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
dynacast: true,
|
dynacast: true,
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
// todo - explain why
|
videoCodec: 'vp9',
|
||||||
red: !e2eeEnabled,
|
|
||||||
videoCodec,
|
|
||||||
},
|
},
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
deviceId: userConfig.videoDeviceId ?? undefined,
|
deviceId: userConfig.videoDeviceId ?? undefined,
|
||||||
@@ -121,35 +76,12 @@ export const Conference = ({
|
|||||||
audioCaptureDefaults: {
|
audioCaptureDefaults: {
|
||||||
deviceId: userConfig.audioDeviceId ?? undefined,
|
deviceId: userConfig.audioDeviceId ?? undefined,
|
||||||
},
|
},
|
||||||
e2ee,
|
|
||||||
}
|
}
|
||||||
// do not rely on the userConfig object directly as its reference may change on every render
|
// do not rely on the userConfig object directly as its reference may change on every render
|
||||||
}, [userConfig.videoDeviceId, userConfig.audioDeviceId])
|
}, [userConfig.videoDeviceId, userConfig.audioDeviceId])
|
||||||
|
|
||||||
const room = useMemo(() => new Room(roomOptions), [roomOptions])
|
const room = useMemo(() => new Room(roomOptions), [roomOptions])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('enter', e2eePassphrase)
|
|
||||||
if (e2eePassphrase) {
|
|
||||||
const keyProvider = getKeyProvider()
|
|
||||||
keyProvider
|
|
||||||
.setKey(e2eePassphrase)
|
|
||||||
.then(() => {
|
|
||||||
room.setE2EEEnabled(true).catch((e) => {
|
|
||||||
if (e instanceof DeviceUnsupportedError) {
|
|
||||||
alert(
|
|
||||||
`You're trying to join an encrypted meeting, but your browser does not support it. Please update it to the latest version and try again.`
|
|
||||||
)
|
|
||||||
console.error(e)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(() => setE2eeSetupComplete(true))
|
|
||||||
}
|
|
||||||
}, [room, e2eePassphrase])
|
|
||||||
|
|
||||||
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
||||||
|
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
@@ -170,10 +102,6 @@ export const Conference = ({
|
|||||||
peerConnectionTimeout: 60000, // Default: 15s. Extended for slow TURN/TLS negotiation
|
peerConnectionTimeout: 60000, // Default: 15s. Extended for slow TURN/TLS negotiation
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEncryptionError = () => {
|
|
||||||
console.log('error')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryAware status={isFetchError ? createStatus : fetchStatus}>
|
<QueryAware status={isFetchError ? createStatus : fetchStatus}>
|
||||||
<Screen header={false} footer={false}>
|
<Screen header={false} footer={false}>
|
||||||
@@ -181,14 +109,13 @@ export const Conference = ({
|
|||||||
room={room}
|
room={room}
|
||||||
serverUrl={data?.livekit?.url}
|
serverUrl={data?.livekit?.url}
|
||||||
token={data?.livekit?.token}
|
token={data?.livekit?.token}
|
||||||
connect={e2eeSetupComplete}
|
connect={true}
|
||||||
audio={userConfig.audioEnabled}
|
audio={userConfig.audioEnabled}
|
||||||
video={userConfig.videoEnabled}
|
video={userConfig.videoEnabled}
|
||||||
connectOptions={connectOptions}
|
connectOptions={connectOptions}
|
||||||
className={css({
|
className={css({
|
||||||
backgroundColor: 'primaryDark.50 !important',
|
backgroundColor: 'primaryDark.50 !important',
|
||||||
})}
|
})}
|
||||||
onEncryptionError={handleEncryptionError}
|
|
||||||
>
|
>
|
||||||
<VideoConference />
|
<VideoConference />
|
||||||
{showInviteDialog && (
|
{showInviteDialog && (
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ import {
|
|||||||
VideoTrack,
|
VideoTrack,
|
||||||
TrackRefContext,
|
TrackRefContext,
|
||||||
ParticipantContextIfNeeded,
|
ParticipantContextIfNeeded,
|
||||||
useIsSpeaking,
|
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import React, { useEffect } from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
isTrackReference,
|
isTrackReference,
|
||||||
isTrackReferencePinned,
|
isTrackReferencePinned,
|
||||||
|
|||||||
@@ -5,12 +5,8 @@ import { css } from '@/styled-system/css'
|
|||||||
import { ToggleButton } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { chatStore } from '@/stores/chat'
|
import { chatStore } from '@/stores/chat'
|
||||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|
||||||
|
|
||||||
export const ChatToggle = ({
|
export const ChatToggle = () => {
|
||||||
onPress,
|
|
||||||
...props
|
|
||||||
}: Partial<ToggleButtonProps>) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
|
||||||
|
|
||||||
const chatSnap = useSnapshot(chatStore)
|
const chatSnap = useSnapshot(chatStore)
|
||||||
@@ -31,12 +27,8 @@ export const ChatToggle = ({
|
|||||||
aria-label={t(tooltipLabel)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isChatOpen}
|
isSelected={isChatOpen}
|
||||||
onPress={(e) => {
|
onPress={() => toggleChat()}
|
||||||
toggleChat()
|
|
||||||
onPress?.(e)
|
|
||||||
}}
|
|
||||||
data-attr={`controls-chat-${tooltipLabel}`}
|
data-attr={`controls-chat-${tooltipLabel}`}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<RiChat1Line />
|
<RiChat1Line />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
+11
-2
@@ -1,11 +1,16 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiMore2Line } from '@remixicon/react'
|
import { RiMore2Line } from '@remixicon/react'
|
||||||
import { Button, Menu } from '@/primitives'
|
import { Button, Menu } from '@/primitives'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems'
|
import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems'
|
||||||
|
import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended'
|
||||||
|
|
||||||
|
export type DialogState = 'username' | 'settings' | null
|
||||||
|
|
||||||
export const OptionsButton = () => {
|
export const OptionsButton = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<DialogState>(null)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu>
|
<Menu>
|
||||||
@@ -17,8 +22,12 @@ export const OptionsButton = () => {
|
|||||||
>
|
>
|
||||||
<RiMore2Line />
|
<RiMore2Line />
|
||||||
</Button>
|
</Button>
|
||||||
<OptionsMenuItems />
|
<OptionsMenuItems onOpenDialog={setDialogOpen} />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<SettingsDialogExtended
|
||||||
|
isOpen={dialogOpen === 'settings'}
|
||||||
|
onOpenChange={(v) => !v && setDialogOpen(null)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -5,17 +5,20 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
|
import { DialogState } from './OptionsButton'
|
||||||
import { Separator } from '@/primitives/Separator'
|
import { Separator } from '@/primitives/Separator'
|
||||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||||
import { useSettingsDialog } from '../SettingsDialogContext'
|
|
||||||
import { GRIST_FORM } from '@/utils/constants'
|
|
||||||
|
|
||||||
// @todo try refactoring it to use MenuList component
|
// @todo try refactoring it to use MenuList component
|
||||||
export const OptionsMenuItems = () => {
|
export const OptionsMenuItems = ({
|
||||||
|
onOpenDialog,
|
||||||
|
}: {
|
||||||
|
onOpenDialog: Dispatch<SetStateAction<DialogState>>
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
const { toggleEffects } = useSidePanel()
|
const { toggleEffects } = useSidePanel()
|
||||||
const { setDialogOpen } = useSettingsDialog()
|
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
style={{
|
style={{
|
||||||
@@ -35,7 +38,7 @@ export const OptionsMenuItems = () => {
|
|||||||
<Separator />
|
<Separator />
|
||||||
<Section>
|
<Section>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href={GRIST_FORM}
|
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={menuRecipe({ icon: true }).item}
|
className={menuRecipe({ icon: true }).item}
|
||||||
>
|
>
|
||||||
@@ -44,7 +47,7 @@ export const OptionsMenuItems = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuRecipe({ icon: true }).item}
|
className={menuRecipe({ icon: true }).item}
|
||||||
onAction={() => setDialogOpen(true)}
|
onAction={() => onOpenDialog('settings')}
|
||||||
>
|
>
|
||||||
<RiSettings3Line size={20} />
|
<RiSettings3Line size={20} />
|
||||||
{t('settings')}
|
{t('settings')}
|
||||||
|
|||||||
+2
-10
@@ -4,12 +4,8 @@ import { ToggleButton } from '@/primitives'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useParticipants } from '@livekit/components-react'
|
import { useParticipants } from '@livekit/components-react'
|
||||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|
||||||
|
|
||||||
export const ParticipantsToggle = ({
|
export const ParticipantsToggle = () => {
|
||||||
onPress,
|
|
||||||
...props
|
|
||||||
}: ToggleButtonProps) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,12 +33,8 @@ export const ParticipantsToggle = ({
|
|||||||
aria-label={t(tooltipLabel)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isParticipantsOpen}
|
isSelected={isParticipantsOpen}
|
||||||
onPress={(e) => {
|
onPress={() => toggleParticipants()}
|
||||||
toggleParticipants()
|
|
||||||
onPress?.(e)
|
|
||||||
}}
|
|
||||||
data-attr={`controls-participants-${tooltipLabel}`}
|
data-attr={`controls-participants-${tooltipLabel}`}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<RiGroupLine />
|
<RiGroupLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -4,21 +4,13 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|
||||||
|
|
||||||
type Props = Omit<
|
export const ScreenShareToggle = (
|
||||||
UseTrackToggleProps<Track.Source.ScreenShare>,
|
props: Omit<
|
||||||
'source' | 'captureOptions'
|
UseTrackToggleProps<Track.Source.ScreenShare>,
|
||||||
> &
|
'source' | 'captureOptions'
|
||||||
Pick<NonNullable<ButtonRecipeProps>, 'variant'> &
|
>
|
||||||
ToggleButtonProps
|
) => {
|
||||||
|
|
||||||
export const ScreenShareToggle = ({
|
|
||||||
variant = 'primaryDark',
|
|
||||||
onPress,
|
|
||||||
...props
|
|
||||||
}: Props) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
|
||||||
const { buttonProps, enabled } = useTrackToggle({
|
const { buttonProps, enabled } = useTrackToggle({
|
||||||
...props,
|
...props,
|
||||||
@@ -34,16 +26,18 @@ export const ScreenShareToggle = ({
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
isSelected={enabled}
|
isSelected={enabled}
|
||||||
square
|
square
|
||||||
variant={variant}
|
variant="primaryDark"
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
onPress={(e) => {
|
onPress={(e) =>
|
||||||
buttonProps.onClick?.(
|
buttonProps.onClick?.(
|
||||||
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
|
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
)
|
)
|
||||||
onPress?.(e)
|
}
|
||||||
|
style={{
|
||||||
|
maxWidth: '46px',
|
||||||
|
maxHeight: '46px',
|
||||||
}}
|
}}
|
||||||
data-attr={`controls-screenshare-${tooltipLabel}`}
|
data-attr={`controls-screenshare-${tooltipLabel}`}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<Div position="relative">
|
<Div position="relative">
|
||||||
<RiRectangleLine size={28} />
|
<RiRectangleLine size={28} />
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended'
|
|
||||||
import React, { createContext, useContext, useState } from 'react'
|
|
||||||
|
|
||||||
const SettingsDialogContext = createContext<
|
|
||||||
| {
|
|
||||||
dialogOpen: boolean
|
|
||||||
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
export const SettingsDialogProvider: React.FC<{
|
|
||||||
children: React.ReactNode
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false)
|
|
||||||
return (
|
|
||||||
<SettingsDialogContext.Provider value={{ dialogOpen, setDialogOpen }}>
|
|
||||||
{children}
|
|
||||||
<SettingsDialogExtended
|
|
||||||
isOpen={dialogOpen}
|
|
||||||
onOpenChange={(v) => !v && setDialogOpen(false)}
|
|
||||||
/>
|
|
||||||
</SettingsDialogContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
export const useSettingsDialog = () => {
|
|
||||||
const context = useContext(SettingsDialogContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useSettingsDialog must be used within a SettingsDialogProvider'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,8 @@ import { RiQuestionLine } from '@remixicon/react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Crisp } from 'crisp-sdk-web'
|
import { Crisp } from 'crisp-sdk-web'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|
||||||
|
|
||||||
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
|
export const SupportToggle = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
||||||
const [isOpened, setIsOpened] = useState($crisp.is('chat:opened'))
|
const [isOpened, setIsOpened] = useState($crisp.is('chat:opened'))
|
||||||
|
|
||||||
@@ -33,16 +32,8 @@ export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
|
|||||||
tooltip={t('support')}
|
tooltip={t('support')}
|
||||||
aria-label={t('support')}
|
aria-label={t('support')}
|
||||||
isSelected={isOpened}
|
isSelected={isOpened}
|
||||||
onPress={(e) => {
|
onPress={() => (isOpened ? Crisp.chat.close() : Crisp.chat.open())}
|
||||||
if (isOpened) {
|
|
||||||
Crisp.chat.close()
|
|
||||||
} else {
|
|
||||||
Crisp.chat.open()
|
|
||||||
}
|
|
||||||
onPress?.(e)
|
|
||||||
}}
|
|
||||||
data-attr="controls-support"
|
data-attr="controls-support"
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<RiQuestionLine />
|
<RiQuestionLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -4,13 +4,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||||
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
|
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|
||||||
|
|
||||||
export const TranscriptToggle = ({
|
export const TranscriptToggle = () => {
|
||||||
variant = 'primaryDark',
|
|
||||||
onPress,
|
|
||||||
...props
|
|
||||||
}: ToggleButtonProps) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
|
||||||
|
|
||||||
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
|
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
|
||||||
@@ -29,15 +24,11 @@ export const TranscriptToggle = ({
|
|||||||
>
|
>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
square
|
square
|
||||||
variant={variant}
|
variant="primaryTextDark"
|
||||||
aria-label={t(tooltipLabel)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isTranscriptOpen}
|
isSelected={isTranscriptOpen}
|
||||||
onPress={(e) => {
|
onPress={() => toggleTranscript()}
|
||||||
toggleTranscript()
|
|
||||||
onPress?.(e)
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<RiBardLine />
|
<RiBardLine />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { supportsScreenSharing } from '@livekit/components-core'
|
||||||
|
|
||||||
|
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||||
|
|
||||||
|
import { StartMediaButton } from '../components/controls/StartMediaButton'
|
||||||
|
import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
||||||
|
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
|
||||||
|
import { ChatToggle } from '../components/controls/ChatToggle'
|
||||||
|
import { HandToggle } from '../components/controls/HandToggle'
|
||||||
|
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
|
||||||
|
import { LeaveButton } from '../components/controls/LeaveButton'
|
||||||
|
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
|
||||||
|
import { SupportToggle } from '../components/controls/SupportToggle'
|
||||||
|
import { TranscriptToggle } from '../components/controls/TranscriptToggle'
|
||||||
|
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type ControlBarControls = {
|
||||||
|
microphone?: boolean
|
||||||
|
camera?: boolean
|
||||||
|
chat?: boolean
|
||||||
|
screenShare?: boolean
|
||||||
|
leave?: boolean
|
||||||
|
settings?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
onDeviceError?: (error: { source: Track.Source; error: Error }) => void
|
||||||
|
variation?: 'minimal' | 'verbose' | 'textOnly'
|
||||||
|
controls?: ControlBarControls
|
||||||
|
/**
|
||||||
|
* If `true`, the user's device choices will be persisted.
|
||||||
|
* This will enable the user to have the same device choices when they rejoin the room.
|
||||||
|
* @defaultValue true
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
saveUserChoices?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `ControlBar` prefab gives the user the basic user interface to control their
|
||||||
|
* media devices (camera, microphone and screen share), open the `Chat` and leave the room.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This component is build with other LiveKit components like `TrackToggle`,
|
||||||
|
* `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <LiveKitRoom>
|
||||||
|
* <ControlBar />
|
||||||
|
* </LiveKitRoom>
|
||||||
|
* ```
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function ControlBar({
|
||||||
|
saveUserChoices = true,
|
||||||
|
onDeviceError,
|
||||||
|
}: ControlBarProps) {
|
||||||
|
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||||
|
|
||||||
|
const {
|
||||||
|
saveAudioInputEnabled,
|
||||||
|
saveVideoInputEnabled,
|
||||||
|
saveAudioInputDeviceId,
|
||||||
|
saveVideoInputDeviceId,
|
||||||
|
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
|
||||||
|
|
||||||
|
const microphoneOnChange = React.useCallback(
|
||||||
|
(enabled: boolean, isUserInitiated: boolean) =>
|
||||||
|
isUserInitiated ? saveAudioInputEnabled(enabled) : null,
|
||||||
|
[saveAudioInputEnabled]
|
||||||
|
)
|
||||||
|
|
||||||
|
const cameraOnChange = React.useCallback(
|
||||||
|
(enabled: boolean, isUserInitiated: boolean) =>
|
||||||
|
isUserInitiated ? saveVideoInputEnabled(enabled) : null,
|
||||||
|
[saveVideoInputEnabled]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '100vw',
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
padding: '1.125rem',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.65rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Microphone}
|
||||||
|
onChange={microphoneOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Camera}
|
||||||
|
onChange={cameraOnChange}
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
|
}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{browserSupportsScreenSharing && (
|
||||||
|
<ScreenShareToggle
|
||||||
|
onDeviceError={(error) =>
|
||||||
|
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HandToggle />
|
||||||
|
<OptionsButton />
|
||||||
|
<LeaveButton />
|
||||||
|
<StartMediaButton />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flex: '1 1 33%',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
paddingRight: '0.25rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ChatToggle />
|
||||||
|
<ParticipantsToggle />
|
||||||
|
<TranscriptToggle />
|
||||||
|
<SupportToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { Track } from 'livekit-client'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
|
||||||
|
|
||||||
import { MobileControlBar } from './MobileControlBar'
|
|
||||||
import { DesktopControlBar } from './DesktopControlBar'
|
|
||||||
import { SettingsDialogProvider } from '../../components/controls/SettingsDialogContext'
|
|
||||||
import { useIsMobile } from '@/utils/useIsMobile'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type ControlBarControls = {
|
|
||||||
microphone?: boolean
|
|
||||||
camera?: boolean
|
|
||||||
chat?: boolean
|
|
||||||
screenShare?: boolean
|
|
||||||
leave?: boolean
|
|
||||||
settings?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
onDeviceError?: (error: { source: Track.Source; error: Error }) => void
|
|
||||||
variation?: 'minimal' | 'verbose' | 'textOnly'
|
|
||||||
controls?: ControlBarControls
|
|
||||||
/**
|
|
||||||
* If `true`, the user's device choices will be persisted.
|
|
||||||
* This will enable the user to have the same device choices when they rejoin the room.
|
|
||||||
* @defaultValue true
|
|
||||||
* @alpha
|
|
||||||
*/
|
|
||||||
saveUserChoices?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `ControlBar` prefab gives the user the basic user interface to control their
|
|
||||||
* media devices (camera, microphone and screen share), open the `Chat` and leave the room.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This component is build with other LiveKit components like `TrackToggle`,
|
|
||||||
* `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* <LiveKitRoom>
|
|
||||||
* <ControlBar />
|
|
||||||
* </LiveKitRoom>
|
|
||||||
* ```
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export function ControlBar({
|
|
||||||
saveUserChoices = true,
|
|
||||||
onDeviceError,
|
|
||||||
}: ControlBarProps) {
|
|
||||||
const {
|
|
||||||
saveAudioInputEnabled,
|
|
||||||
saveVideoInputEnabled,
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
|
|
||||||
|
|
||||||
const microphoneOnChange = React.useCallback(
|
|
||||||
(enabled: boolean, isUserInitiated: boolean) =>
|
|
||||||
isUserInitiated ? saveAudioInputEnabled(enabled) : null,
|
|
||||||
[saveAudioInputEnabled]
|
|
||||||
)
|
|
||||||
|
|
||||||
const cameraOnChange = React.useCallback(
|
|
||||||
(enabled: boolean, isUserInitiated: boolean) =>
|
|
||||||
isUserInitiated ? saveVideoInputEnabled(enabled) : null,
|
|
||||||
[saveVideoInputEnabled]
|
|
||||||
)
|
|
||||||
|
|
||||||
const barProps = {
|
|
||||||
onDeviceError,
|
|
||||||
microphoneOnChange,
|
|
||||||
cameraOnChange,
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsDialogProvider>
|
|
||||||
{isMobile ? (
|
|
||||||
<MobileControlBar {...barProps} />
|
|
||||||
) : (
|
|
||||||
<DesktopControlBar {...barProps} />
|
|
||||||
)}
|
|
||||||
</SettingsDialogProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ControlBarAuxProps {
|
|
||||||
onDeviceError: ControlBarProps['onDeviceError']
|
|
||||||
microphoneOnChange: (
|
|
||||||
enabled: boolean,
|
|
||||||
isUserInitiated: boolean
|
|
||||||
) => void | null
|
|
||||||
cameraOnChange: (enabled: boolean, isUserInitiated: boolean) => void | null
|
|
||||||
saveAudioInputDeviceId: (deviceId: string) => void
|
|
||||||
saveVideoInputDeviceId: (deviceId: string) => void
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { supportsScreenSharing } from '@livekit/components-core'
|
|
||||||
import { ControlBarAuxProps } from './ControlBar'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { LeaveButton } from '../../components/controls/LeaveButton'
|
|
||||||
import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice'
|
|
||||||
import { Track } from 'livekit-client'
|
|
||||||
import { HandToggle } from '../../components/controls/HandToggle'
|
|
||||||
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
|
|
||||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
|
||||||
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
|
||||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
|
||||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
|
||||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
|
||||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
|
||||||
|
|
||||||
export function DesktopControlBar({
|
|
||||||
onDeviceError,
|
|
||||||
microphoneOnChange,
|
|
||||||
cameraOnChange,
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
}: ControlBarAuxProps) {
|
|
||||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
width: '100vw',
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
padding: '1.125rem',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
marginLeft: '0.5rem',
|
|
||||||
})}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.65rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Microphone}
|
|
||||||
onChange={microphoneOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Camera}
|
|
||||||
onChange={cameraOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Camera, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{browserSupportsScreenSharing && (
|
|
||||||
<ScreenShareToggle
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<HandToggle />
|
|
||||||
<OptionsButton />
|
|
||||||
<LeaveButton />
|
|
||||||
<StartMediaButton />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
flex: '1 1 33%',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
paddingRight: '0.25rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ChatToggle />
|
|
||||||
<ParticipantsToggle />
|
|
||||||
<TranscriptToggle />
|
|
||||||
<SupportToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { supportsScreenSharing } from '@livekit/components-core'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { ControlBarAuxProps } from './ControlBar'
|
|
||||||
import React from 'react'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { LeaveButton } from '../../components/controls/LeaveButton'
|
|
||||||
import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice'
|
|
||||||
import { Track } from 'livekit-client'
|
|
||||||
import { HandToggle } from '../../components/controls/HandToggle'
|
|
||||||
import { Button } from '@/primitives/Button'
|
|
||||||
import {
|
|
||||||
RiAccountBoxLine,
|
|
||||||
RiMegaphoneLine,
|
|
||||||
RiMore2Line,
|
|
||||||
RiSettings3Line,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
|
|
||||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
|
||||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
|
||||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
|
||||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
|
||||||
import { LinkButton } from '@/primitives'
|
|
||||||
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
|
|
||||||
import { ResponsiveMenu } from './ResponsiveMenu'
|
|
||||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
|
||||||
|
|
||||||
export function MobileControlBar({
|
|
||||||
onDeviceError,
|
|
||||||
microphoneOnChange,
|
|
||||||
cameraOnChange,
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
}: ControlBarAuxProps) {
|
|
||||||
const { t } = useTranslation('rooms')
|
|
||||||
const [isMenuOpened, setIsMenuOpened] = React.useState(false)
|
|
||||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
|
||||||
const { toggleEffects } = useSidePanel()
|
|
||||||
const { setDialogOpen } = useSettingsDialog()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
width: '100vw',
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
padding: '1.125rem',
|
|
||||||
justifyContent: 'center',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '422px',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<LeaveButton />
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Microphone}
|
|
||||||
onChange={microphoneOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Camera}
|
|
||||||
onChange={cameraOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Camera, error })
|
|
||||||
}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<HandToggle />
|
|
||||||
<Button
|
|
||||||
square
|
|
||||||
variant="primaryDark"
|
|
||||||
aria-label={t('options.buttonLabel')}
|
|
||||||
tooltip={t('options.buttonLabel')}
|
|
||||||
onPress={() => setIsMenuOpened(true)}
|
|
||||||
>
|
|
||||||
<RiMore2Line />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResponsiveMenu
|
|
||||||
isOpened={isMenuOpened}
|
|
||||||
onClosed={() => setIsMenuOpened(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
|
|
||||||
gridGap: '1rem',
|
|
||||||
'& > *': {
|
|
||||||
alignSelf: 'center',
|
|
||||||
justifySelf: 'center',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{browserSupportsScreenSharing && (
|
|
||||||
<ScreenShareToggle
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.ScreenShare, error })
|
|
||||||
}
|
|
||||||
variant="primaryTextDark"
|
|
||||||
description={true}
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ChatToggle
|
|
||||||
description={true}
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
/>
|
|
||||||
<ParticipantsToggle
|
|
||||||
description={true}
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
/>
|
|
||||||
<TranscriptToggle
|
|
||||||
description={true}
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
/>
|
|
||||||
<SupportToggle
|
|
||||||
description={true}
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
toggleEffects()
|
|
||||||
setIsMenuOpened(false)
|
|
||||||
}}
|
|
||||||
variant="primaryTextDark"
|
|
||||||
aria-label={t('options.items.effects')}
|
|
||||||
tooltip={t('options.items.effects')}
|
|
||||||
description={true}
|
|
||||||
>
|
|
||||||
<RiAccountBoxLine size={20} />
|
|
||||||
</Button>
|
|
||||||
<LinkButton
|
|
||||||
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
|
||||||
variant="primaryTextDark"
|
|
||||||
tooltip={t('options.items.feedbacks')}
|
|
||||||
aria-label={t('options.items.feedbacks')}
|
|
||||||
description={true}
|
|
||||||
target="_blank"
|
|
||||||
onPress={() => setIsMenuOpened(false)}
|
|
||||||
>
|
|
||||||
<RiMegaphoneLine size={20} />
|
|
||||||
</LinkButton>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
setDialogOpen(true)
|
|
||||||
setIsMenuOpened(false)
|
|
||||||
}}
|
|
||||||
variant="primaryTextDark"
|
|
||||||
aria-label={t('options.items.settings')}
|
|
||||||
tooltip={t('options.items.settings')}
|
|
||||||
description={true}
|
|
||||||
>
|
|
||||||
<RiSettings3Line size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResponsiveMenu>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { PropsWithChildren } from 'react'
|
|
||||||
import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
|
|
||||||
|
|
||||||
interface ResponsiveMenuProps extends PropsWithChildren {
|
|
||||||
isOpened: boolean
|
|
||||||
onClosed: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResponsiveMenu({
|
|
||||||
isOpened,
|
|
||||||
onClosed,
|
|
||||||
children,
|
|
||||||
}: ResponsiveMenuProps) {
|
|
||||||
return (
|
|
||||||
<ModalOverlay
|
|
||||||
isDismissable
|
|
||||||
isOpen={isOpened}
|
|
||||||
onOpenChange={(isOpened) => {
|
|
||||||
if (!isOpened) {
|
|
||||||
onClosed()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={css({
|
|
||||||
width: '100vw',
|
|
||||||
height: 'var(--visual-viewport-height)',
|
|
||||||
zIndex: 100,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
display: 'flex',
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
padding: '1.5rem 1.5rem 1rem 1.5rem',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Modal
|
|
||||||
className={css({
|
|
||||||
backgroundColor: 'primaryDark.200',
|
|
||||||
borderRadius: '20px',
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: '1.5rem',
|
|
||||||
'&[data-entering]': {
|
|
||||||
animation: 'slide-full 200ms',
|
|
||||||
},
|
|
||||||
'&[data-exiting]': {
|
|
||||||
animation: 'slide-full 200ms reverse',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Dialog>{children}</Dialog>
|
|
||||||
</Modal>
|
|
||||||
</ModalOverlay>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
useCreateLayoutContext,
|
useCreateLayoutContext,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
|
|
||||||
import { ControlBar } from './ControlBar/ControlBar'
|
import { ControlBar } from './ControlBar'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { cva } from '@/styled-system/css'
|
import { cva } from '@/styled-system/css'
|
||||||
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import { Heading } from 'react-aria-components'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiAccountCircleLine,
|
RiAccountCircleLine,
|
||||||
RiNotification3Line,
|
|
||||||
RiSettings3Line,
|
RiSettings3Line,
|
||||||
RiSpeakerLine,
|
RiSpeakerLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { AccountTab } from './tabs/AccountTab'
|
import { AccountTab } from './tabs/AccountTab'
|
||||||
import { NotificationsTab } from './tabs/NotificationsTab'
|
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
|
||||||
import { GeneralTab } from './tabs/GeneralTab'
|
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
|
||||||
import { AudioTab } from './tabs/AudioTab'
|
|
||||||
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
@@ -83,17 +81,12 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
|||||||
<RiSettings3Line />
|
<RiSettings3Line />
|
||||||
{isWideScreen && t('tabs.general')}
|
{isWideScreen && t('tabs.general')}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab icon highlight id="4">
|
|
||||||
<RiNotification3Line />
|
|
||||||
{isWideScreen && t('tabs.notifications')}
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
</TabList>
|
||||||
</div>
|
</div>
|
||||||
<div className={tabPanelContainerStyle}>
|
<div className={tabPanelContainerStyle}>
|
||||||
<AccountTab id="1" onOpenChange={props.onOpenChange} />
|
<AccountTab id="1" onOpenChange={props.onOpenChange} />
|
||||||
<AudioTab id="2" />
|
<AudioTab id="2" />
|
||||||
<GeneralTab id="3" />
|
<GeneralTab id="3" />
|
||||||
<NotificationsTab id="4" />
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
|
||||||
import { H, Switch } from '@/primitives'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { notificationsStore } from '@/stores/notifications'
|
|
||||||
|
|
||||||
export type NotificationsTabProps = Pick<TabPanelProps, 'id'>
|
|
||||||
|
|
||||||
export const NotificationsTab = ({ id }: NotificationsTabProps) => {
|
|
||||||
const { t } = useTranslation('settings', { keyPrefix: 'notifications' })
|
|
||||||
const notificationsSnap = useSnapshot(notificationsStore)
|
|
||||||
return (
|
|
||||||
<TabPanel padding={'md'} flex id={id}>
|
|
||||||
<H lvl={2}>{t('heading')}</H>
|
|
||||||
<ul
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '1rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{Array.from(notificationsSnap.soundNotifications).map(
|
|
||||||
([key, value]) => (
|
|
||||||
<li key={key}>
|
|
||||||
<Switch
|
|
||||||
aria-label={`${t(`actions.${value ? 'disable' : 'enable'}`)} ${t('label')} "${t(`items.${key}`)}"`}
|
|
||||||
isSelected={value}
|
|
||||||
onChange={(v) => {
|
|
||||||
notificationsStore.soundNotifications.set(key, v)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t(`items.${key}`)}
|
|
||||||
</Switch>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</TabPanel>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -19,19 +19,6 @@
|
|||||||
},
|
},
|
||||||
"permissionsRequired": ""
|
"permissionsRequired": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
|
||||||
"heading": "",
|
|
||||||
"label": "",
|
|
||||||
"actions": {
|
|
||||||
"disable": "",
|
|
||||||
"enable": ""
|
|
||||||
},
|
|
||||||
"items": {
|
|
||||||
"participantJoined": "",
|
|
||||||
"handRaised": "",
|
|
||||||
"messageReceived": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": ""
|
"heading": ""
|
||||||
},
|
},
|
||||||
@@ -43,7 +30,6 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"account": "",
|
"account": "",
|
||||||
"audio": "",
|
"audio": "",
|
||||||
"general": "",
|
"general": ""
|
||||||
"notifications": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,19 +19,6 @@
|
|||||||
},
|
},
|
||||||
"permissionsRequired": "Permissions required"
|
"permissionsRequired": "Permissions required"
|
||||||
},
|
},
|
||||||
"notifications": {
|
|
||||||
"heading": "Sound notifications",
|
|
||||||
"label": "sound notifications for",
|
|
||||||
"actions": {
|
|
||||||
"disable": "Disable",
|
|
||||||
"enable": "Enable"
|
|
||||||
},
|
|
||||||
"items": {
|
|
||||||
"participantJoined": "Participant joined",
|
|
||||||
"handRaised": "Hand raised",
|
|
||||||
"messageReceived": "Message received"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": "Settings"
|
"heading": "Settings"
|
||||||
},
|
},
|
||||||
@@ -43,7 +30,6 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"account": "Profile",
|
"account": "Profile",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"general": "General",
|
"general": "General"
|
||||||
"notifications": "Notifications"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,19 +19,6 @@
|
|||||||
},
|
},
|
||||||
"permissionsRequired": "Autorisations nécessaires"
|
"permissionsRequired": "Autorisations nécessaires"
|
||||||
},
|
},
|
||||||
"notifications": {
|
|
||||||
"heading": "Notifications sonores",
|
|
||||||
"label": "la notification sonore pour",
|
|
||||||
"actions": {
|
|
||||||
"disable": "Désactiver",
|
|
||||||
"enable": "Activer"
|
|
||||||
},
|
|
||||||
"items": {
|
|
||||||
"participantJoined": "Un nouveau participant",
|
|
||||||
"handRaised": "Une main levée",
|
|
||||||
"messageReceived": "Un message reçu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": "Paramètres"
|
"heading": "Paramètres"
|
||||||
},
|
},
|
||||||
@@ -43,7 +30,6 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"account": "Profile",
|
"account": "Profile",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"general": "Général",
|
"general": "Général"
|
||||||
"notifications": "Notifications"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import {
|
|||||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||||
RACButtonsProps &
|
RACButtonsProps &
|
||||||
TooltipWrapperProps & {
|
TooltipWrapperProps
|
||||||
// Use tooltip as description below the button.
|
|
||||||
description?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button = ({
|
export const Button = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
@@ -26,10 +22,7 @@ export const Button = ({
|
|||||||
<RACButton
|
<RACButton
|
||||||
className={buttonRecipe(variantProps)}
|
className={buttonRecipe(variantProps)}
|
||||||
{...(componentProps as RACButtonsProps)}
|
{...(componentProps as RACButtonsProps)}
|
||||||
>
|
/>
|
||||||
{componentProps.children as ReactNode}
|
|
||||||
{props.description && <span>{tooltip}</span>}
|
|
||||||
</RACButton>
|
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import { Link, LinkProps } from 'react-aria-components'
|
|||||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||||
LinkProps &
|
LinkProps &
|
||||||
TooltipWrapperProps & {
|
TooltipWrapperProps
|
||||||
// Use tooltip as description below the button.
|
|
||||||
description?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkButton = ({
|
export const LinkButton = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
@@ -20,12 +16,7 @@ export const LinkButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
<Link className={buttonRecipe(variantProps)} {...componentProps}>
|
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||||
<>
|
|
||||||
{componentProps.children as ReactNode}
|
|
||||||
{props.description && <span>{tooltip}</span>}
|
|
||||||
</>
|
|
||||||
</Link>
|
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
Switch as RACSwitch,
|
|
||||||
SwitchProps as RACSwitchProps,
|
|
||||||
} from 'react-aria-components'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { StyledVariantProps } from '@/styled-system/types'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export const StyledSwitch = styled(RACSwitch, {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.571rem',
|
|
||||||
color: 'black',
|
|
||||||
forcedColorAdjust: 'none',
|
|
||||||
'& .indicator': {
|
|
||||||
width: '2.6rem',
|
|
||||||
height: '1.563rem',
|
|
||||||
border: '0.125rem solid',
|
|
||||||
borderColor: 'primary.800',
|
|
||||||
borderRadius: '1.143rem',
|
|
||||||
transition: 'all 200ms, outline 200ms',
|
|
||||||
_before: {
|
|
||||||
content: '""',
|
|
||||||
display: 'block',
|
|
||||||
margin: '0.125rem',
|
|
||||||
width: '1.063rem',
|
|
||||||
height: '1.063rem',
|
|
||||||
borderRadius: '1.063rem',
|
|
||||||
background: 'primary.800',
|
|
||||||
transition: 'all 200ms',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'&[data-selected] .indicator': {
|
|
||||||
borderColor: 'primary.800',
|
|
||||||
background: 'primary.800',
|
|
||||||
_before: {
|
|
||||||
background: 'white',
|
|
||||||
transform: 'translateX(100%)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'&[data-disabled] .indicator': {
|
|
||||||
borderColor: 'primary.200',
|
|
||||||
background: 'transparent',
|
|
||||||
_before: {
|
|
||||||
background: 'primary.200',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'&[data-focus-visible] .indicator': {
|
|
||||||
outline: '2px solid!',
|
|
||||||
outlineColor: 'focusRing!',
|
|
||||||
outlineOffset: '2px!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type SwitchProps = StyledVariantProps<typeof StyledSwitch> &
|
|
||||||
RACSwitchProps & { children: ReactNode }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Styled RAC Switch.
|
|
||||||
*/
|
|
||||||
export const Switch = ({ children, ...props }: SwitchProps) => (
|
|
||||||
<StyledSwitch {...props}>
|
|
||||||
<div className="indicator" />
|
|
||||||
{children}
|
|
||||||
</StyledSwitch>
|
|
||||||
)
|
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
ToggleButton as RACToggleButton,
|
ToggleButton as RACToggleButton,
|
||||||
ToggleButtonProps as RACToggleButtonProps,
|
ToggleButtonProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
|
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
|
||||||
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
|
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export type ToggleButtonProps = RACToggleButtonProps &
|
|
||||||
ButtonRecipeProps &
|
|
||||||
TooltipWrapperProps & {
|
|
||||||
// Use tooltip as description below the button.
|
|
||||||
description?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React aria ToggleButton with our button styles, that can take a tooltip if needed
|
* React aria ToggleButton with our button styles, that can take a tooltip if needed
|
||||||
@@ -20,20 +12,14 @@ export const ToggleButton = ({
|
|||||||
tooltip,
|
tooltip,
|
||||||
tooltipType,
|
tooltipType,
|
||||||
...props
|
...props
|
||||||
}: ToggleButtonProps) => {
|
}: ToggleButtonProps & ButtonRecipeProps & TooltipWrapperProps) => {
|
||||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
<RACToggleButton
|
<RACToggleButton
|
||||||
{...componentProps}
|
{...componentProps}
|
||||||
className={buttonRecipe(variantProps)}
|
className={buttonRecipe(variantProps)}
|
||||||
>
|
/>
|
||||||
<>
|
|
||||||
{componentProps.children as ReactNode}
|
|
||||||
{props.description && <span>{tooltip}</span>}
|
|
||||||
</>
|
|
||||||
</RACToggleButton>
|
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { type RecipeVariantProps, cva } from '@/styled-system/css'
|
import { type RecipeVariantProps, cva } from '@/styled-system/css'
|
||||||
|
|
||||||
|
export type ButtonRecipe = typeof buttonRecipe
|
||||||
|
|
||||||
|
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
||||||
|
|
||||||
export const buttonRecipe = cva({
|
export const buttonRecipe = cva({
|
||||||
base: {
|
base: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -218,16 +222,6 @@ export const buttonRecipe = cva({
|
|||||||
shySelected: {
|
shySelected: {
|
||||||
true: {},
|
true: {},
|
||||||
},
|
},
|
||||||
description: {
|
|
||||||
true: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '0.5rem',
|
|
||||||
'& span': {
|
|
||||||
fontSize: '13px',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// if the button is next to other ones to make a "button group", tell where the button is to handle radius
|
// if the button is next to other ones to make a "button group", tell where the button is to handle radius
|
||||||
groupPosition: {
|
groupPosition: {
|
||||||
left: {
|
left: {
|
||||||
@@ -261,7 +255,3 @@ export const buttonRecipe = cva({
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ButtonRecipe = typeof buttonRecipe
|
|
||||||
|
|
||||||
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
|
||||||
|
|||||||
@@ -29,4 +29,3 @@ export { ToggleButton } from './ToggleButton'
|
|||||||
export { Ul } from './Ul'
|
export { Ul } from './Ul'
|
||||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||||
export { TextArea } from './TextArea'
|
export { TextArea } from './TextArea'
|
||||||
export { Switch } from './Switch'
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { proxy, subscribe } from 'valtio'
|
|
||||||
import { proxyMap } from 'valtio/utils'
|
|
||||||
import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio'
|
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
soundNotifications: Map<NotificationType, boolean>
|
|
||||||
soundNotificationVolume: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_STATE: State = {
|
|
||||||
soundNotifications: proxyMap(
|
|
||||||
new Map([
|
|
||||||
[NotificationType.ParticipantJoined, true],
|
|
||||||
[NotificationType.HandRaised, true],
|
|
||||||
])
|
|
||||||
),
|
|
||||||
soundNotificationVolume: 0.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNotificationsState(): State {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS)
|
|
||||||
if (!stored) return DEFAULT_STATE
|
|
||||||
const parsed = JSON.parse(stored, deserializeToProxyMap)
|
|
||||||
return parsed || DEFAULT_STATE
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error(
|
|
||||||
'[NotificationsStore] Failed to parse stored settings:',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
return DEFAULT_STATE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notificationsStore = proxy<State>(getNotificationsState())
|
|
||||||
|
|
||||||
subscribe(notificationsStore, () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_KEYS.NOTIFICATIONS,
|
|
||||||
JSON.stringify(notificationsStore, serializeProxyMap)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -29,12 +29,3 @@ body,
|
|||||||
body:has(.lk-video-conference) #crisp-chatbox > div > a {
|
body:has(.lk-video-conference) #crisp-chatbox > div > a {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-full {
|
|
||||||
from {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const GRIST_FORM =
|
|
||||||
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4' as const
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* Object containing all localStorage keys used across the app
|
|
||||||
*/
|
|
||||||
export const STORAGE_KEYS = {
|
|
||||||
NOTIFICATIONS: 'app_notification_settings',
|
|
||||||
} as const
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { isMobileBrowser } from '@livekit/components-core'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export const useIsMobile = () => {
|
|
||||||
const [isMobile, setIsMobile] = useState(isMobileBrowser())
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setIsMobile(isMobileBrowser())
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return isMobile
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { proxyMap } from 'valtio/utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes Map objects into a JSON-friendly format while preserving valtio proxyMap compatibility
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function serializeProxyMap(_: string, value: any) {
|
|
||||||
if (value instanceof Map) {
|
|
||||||
return {
|
|
||||||
dataType: 'Map',
|
|
||||||
value: Array.from(value.entries()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom JSON reviver function for deserializing Map objects and wrapping them in valtio proxyMap
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function deserializeToProxyMap(_: string, value: any) {
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
if (value.dataType === 'Map') {
|
|
||||||
return proxyMap(new Map(value.value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
terminationGracePeriodSeconds: 18000
|
|
||||||
|
|
||||||
egress:
|
|
||||||
log_level: debug
|
|
||||||
ws_url: ws://livekit-livekit-server:80
|
|
||||||
insecure: true
|
|
||||||
enable_chrome_sandbox: true
|
|
||||||
{{- with .Values.livekit.keys }}
|
|
||||||
{{- range $key, $value := . }}
|
|
||||||
api_key: {{ $key }}
|
|
||||||
api_secret: {{ $value }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
redis:
|
|
||||||
address: redis-master:6379
|
|
||||||
password: pass
|
|
||||||
s3:
|
|
||||||
access_key: meet
|
|
||||||
secret: password
|
|
||||||
region: local
|
|
||||||
bucket: meet-media-storage
|
|
||||||
endpoint: http://minio:9000
|
|
||||||
force_path_style: true
|
|
||||||
|
|
||||||
loadBalancer:
|
|
||||||
type: nginx
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
|
||||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- livekit-egress.127.0.0.1.nip.io
|
|
||||||
secretName: livekit-egress-dinum-cert
|
|
||||||
|
|
||||||
autoscaling:
|
|
||||||
enabled: false
|
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 5
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
resources: {}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
terminationGracePeriodSeconds: 18000
|
|
||||||
|
|
||||||
livekit:
|
|
||||||
log_level: debug
|
|
||||||
rtc:
|
|
||||||
use_external_ip: false
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 60000
|
|
||||||
tcp_port: 7881
|
|
||||||
redis:
|
|
||||||
address: redis-master:6379
|
|
||||||
password: pass
|
|
||||||
keys:
|
|
||||||
turn:
|
|
||||||
enabled: true
|
|
||||||
udp_port: 443
|
|
||||||
domain: livekit.127.0.0.1.nip.io
|
|
||||||
loadBalancerAnnotations: {}
|
|
||||||
|
|
||||||
|
|
||||||
loadBalancer:
|
|
||||||
type: nginx
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
|
||||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- livekit.127.0.0.1.nip.io
|
|
||||||
secretName: livekit-dinum-cert
|
|
||||||
|
|
||||||
autoscaling:
|
|
||||||
enabled: false
|
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 5
|
|
||||||
targetCPUUtilizationPercentage: 60
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
resources: {}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
image:
|
|
||||||
repository: localhost:5001/meet-backend
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
backend:
|
|
||||||
replicas: 1
|
|
||||||
envVars:
|
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.127.0.0.1.nip.io,http://meet.127.0.0.1.nip.io
|
|
||||||
DJANGO_CONFIGURATION: Production
|
|
||||||
DJANGO_ALLOWED_HOSTS: meet.127.0.0.1.nip.io
|
|
||||||
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
|
|
||||||
DJANGO_SETTINGS_MODULE: meet.settings
|
|
||||||
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
|
|
||||||
DJANGO_SUPERUSER_PASSWORD: admin
|
|
||||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
|
||||||
DJANGO_EMAIL_PORT: 1025
|
|
||||||
DJANGO_EMAIL_USE_SSL: False
|
|
||||||
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/certs
|
|
||||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/auth
|
|
||||||
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/token
|
|
||||||
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/userinfo
|
|
||||||
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/session/end
|
|
||||||
OIDC_RP_CLIENT_ID:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: OIDC_RP_CLIENT_ID
|
|
||||||
OIDC_RP_CLIENT_SECRET:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: OIDC_RP_CLIENT_SECRET
|
|
||||||
OIDC_RP_SIGN_ALGO: RS256
|
|
||||||
OIDC_RP_SCOPES: "openid email"
|
|
||||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.127.0.0.1.nip.io
|
|
||||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
|
||||||
OIDC_VERIFY_SSL: False
|
|
||||||
LOGIN_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
|
||||||
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
|
|
||||||
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
|
||||||
DB_HOST: postgres-postgresql
|
|
||||||
DB_NAME: meet
|
|
||||||
DB_USER: dinum
|
|
||||||
DB_PASSWORD: pass
|
|
||||||
DB_PORT: 5432
|
|
||||||
POSTGRES_DB: meet
|
|
||||||
POSTGRES_USER: dinum
|
|
||||||
POSTGRES_PASSWORD: pass
|
|
||||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
|
||||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
|
||||||
{{- with .Values.livekit.keys }}
|
|
||||||
{{- range $key, $value := . }}
|
|
||||||
LIVEKIT_API_SECRET: {{ $value }}
|
|
||||||
LIVEKIT_API_KEY: {{ $key }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
|
|
||||||
ALLOW_UNREGISTERED_ROOMS: False
|
|
||||||
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
|
|
||||||
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
|
|
||||||
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
|
|
||||||
AWS_S3_ACCESS_KEY_ID: meet
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY: password
|
|
||||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
|
||||||
AWS_S3_REGION_NAME: local
|
|
||||||
RECORDING_ENABLE: True
|
|
||||||
RECORDING_VERIFY_SSL: False
|
|
||||||
RECORDING_STORAGE_EVENT_ENABLE: True
|
|
||||||
RECORDING_STORAGE_EVENT_TOKEN: password
|
|
||||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
|
|
||||||
SUMMARY_SERVICE_API_TOKEN: password
|
|
||||||
|
|
||||||
|
|
||||||
migrate:
|
|
||||||
command:
|
|
||||||
- "/bin/sh"
|
|
||||||
- "-c"
|
|
||||||
- |
|
|
||||||
python manage.py migrate --no-input &&
|
|
||||||
python manage.py create_demo --force
|
|
||||||
restartPolicy: Never
|
|
||||||
|
|
||||||
command:
|
|
||||||
- "gunicorn"
|
|
||||||
- "-c"
|
|
||||||
- "/usr/local/etc/gunicorn/meet.py"
|
|
||||||
- "meet.wsgi:application"
|
|
||||||
- "--reload"
|
|
||||||
|
|
||||||
createsuperuser:
|
|
||||||
command:
|
|
||||||
- "/bin/sh"
|
|
||||||
- "-c"
|
|
||||||
- |
|
|
||||||
python manage.py createsuperuser --email admin@example.com --password admin
|
|
||||||
restartPolicy: Never
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
envVars:
|
|
||||||
VITE_PORT: 8080
|
|
||||||
VITE_HOST: 0.0.0.0
|
|
||||||
VITE_API_BASE_URL: https://meet.127.0.0.1.nip.io/
|
|
||||||
|
|
||||||
replicas: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: localhost:5001/meet-frontend
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
host: meet.127.0.0.1.nip.io
|
|
||||||
|
|
||||||
ingressAdmin:
|
|
||||||
enabled: true
|
|
||||||
host: meet.127.0.0.1.nip.io
|
|
||||||
|
|
||||||
posthog:
|
|
||||||
ingress:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
ingressAssets:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
summary:
|
|
||||||
replicas: 1
|
|
||||||
envVars:
|
|
||||||
APP_NAME: summary-microservice
|
|
||||||
APP_API_TOKEN: password
|
|
||||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
|
||||||
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
|
|
||||||
AWS_S3_ACCESS_KEY_ID: meet
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY: password
|
|
||||||
OPENAI_API_KEY: password
|
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
|
||||||
AWS_S3_SECURE_ACCESS: False
|
|
||||||
WEBHOOK_API_TOKEN: password
|
|
||||||
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
|
||||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
|
||||||
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: localhost:5001/meet-summary
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
command:
|
|
||||||
- "uvicorn"
|
|
||||||
- "summary.main:app"
|
|
||||||
- "--host"
|
|
||||||
- "0.0.0.0"
|
|
||||||
- "--port"
|
|
||||||
- "8000"
|
|
||||||
- "--reload"
|
|
||||||
|
|
||||||
celery:
|
|
||||||
replicas: 1
|
|
||||||
envVars:
|
|
||||||
APP_NAME: summary-microservice
|
|
||||||
APP_API_TOKEN: password
|
|
||||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
|
||||||
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
|
|
||||||
AWS_S3_ACCESS_KEY_ID: meet
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY: password
|
|
||||||
OPENAI_API_KEY: password
|
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
|
||||||
AWS_S3_SECURE_ACCESS: False
|
|
||||||
WEBHOOK_API_TOKEN: password
|
|
||||||
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
|
||||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
|
||||||
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: localhost:5001/meet-summary
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
command:
|
|
||||||
- "celery"
|
|
||||||
- "-A"
|
|
||||||
- "summary.core.celery_worker"
|
|
||||||
- "worker"
|
|
||||||
- "--pool=solo"
|
|
||||||
- "--loglevel=info"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
djangoSecretKey: u!vbjDW71aru&OZA%NZQi0x
|
|
||||||
livekit:
|
|
||||||
keys:
|
|
||||||
devkey: secret
|
|
||||||
livekitApi:
|
|
||||||
key: devkey
|
|
||||||
secret: secret
|
|
||||||
oidc:
|
|
||||||
clientId: meet
|
|
||||||
clientSecret: ThisIsAnExampleKeyForDevPurposeOnly
|
|
||||||
@@ -17,10 +17,6 @@ livekit:
|
|||||||
udp_port: 443
|
udp_port: 443
|
||||||
domain: livekit.127.0.0.1.nip.io
|
domain: livekit.127.0.0.1.nip.io
|
||||||
loadBalancerAnnotations: {}
|
loadBalancerAnnotations: {}
|
||||||
webhook:
|
|
||||||
api_key: devkey
|
|
||||||
urls:
|
|
||||||
- https://meet.127.0.0.1.nip.io/api/v1.0/rooms/livekit-webhook/
|
|
||||||
|
|
||||||
|
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ backend:
|
|||||||
RECORDING_STORAGE_EVENT_TOKEN: password
|
RECORDING_STORAGE_EVENT_TOKEN: password
|
||||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
|
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
|
||||||
SUMMARY_SERVICE_API_TOKEN: password
|
SUMMARY_SERVICE_API_TOKEN: password
|
||||||
PASSPHRASE_ENCRYPTION_KEY: lT3cX5dzFhCe-9xNjXUiTCX00r2ZgHgGUJKO66x-QIo=
|
|
||||||
|
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
@@ -126,7 +125,7 @@ summary:
|
|||||||
AWS_S3_ACCESS_KEY_ID: meet
|
AWS_S3_ACCESS_KEY_ID: meet
|
||||||
AWS_S3_SECRET_ACCESS_KEY: password
|
AWS_S3_SECRET_ACCESS_KEY: password
|
||||||
OPENAI_API_KEY: password
|
OPENAI_API_KEY: password
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
||||||
AWS_S3_SECURE_ACCESS: False
|
AWS_S3_SECURE_ACCESS: False
|
||||||
@@ -159,7 +158,7 @@ celery:
|
|||||||
AWS_S3_ACCESS_KEY_ID: meet
|
AWS_S3_ACCESS_KEY_ID: meet
|
||||||
AWS_S3_SECRET_ACCESS_KEY: password
|
AWS_S3_SECRET_ACCESS_KEY: password
|
||||||
OPENAI_API_KEY: password
|
OPENAI_API_KEY: password
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
||||||
AWS_S3_SECURE_ACCESS: False
|
AWS_S3_SECURE_ACCESS: False
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-backend
|
repository: lasuite/meet-backend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.12"
|
tag: "v0.1.10"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
migrateJobAnnotations:
|
migrateJobAnnotations:
|
||||||
@@ -116,17 +116,6 @@ backend:
|
|||||||
name: meet-media-storage.bucket.libre.sh
|
name: meet-media-storage.bucket.libre.sh
|
||||||
key: bucket
|
key: bucket
|
||||||
AWS_S3_REGION_NAME: local
|
AWS_S3_REGION_NAME: local
|
||||||
RECORDING_ENABLE: True
|
|
||||||
RECORDING_STORAGE_EVENT_ENABLE: True
|
|
||||||
RECORDING_STORAGE_EVENT_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend
|
|
||||||
key: RECORDING_STORAGE_EVENT_TOKEN
|
|
||||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
|
|
||||||
SUMMARY_SERVICE_API_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: APP_API_TOKEN
|
|
||||||
|
|
||||||
createsuperuser:
|
createsuperuser:
|
||||||
command:
|
command:
|
||||||
@@ -140,7 +129,7 @@ frontend:
|
|||||||
image:
|
image:
|
||||||
repository: lasuite/meet-frontend
|
repository: lasuite/meet-frontend
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "v0.1.12"
|
tag: "v0.1.10"
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -178,113 +167,7 @@ posthog:
|
|||||||
nginx.ingress.kubernetes.io/backend-protocol: https
|
nginx.ingress.kubernetes.io/backend-protocol: https
|
||||||
|
|
||||||
summary:
|
summary:
|
||||||
replicas: 1
|
replicas: 0
|
||||||
envVars:
|
|
||||||
APP_NAME: summary-microservice
|
|
||||||
APP_API_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: APP_API_TOKEN
|
|
||||||
AWS_S3_ENDPOINT_URL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: url
|
|
||||||
AWS_S3_ACCESS_KEY_ID:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: accessKey
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: secretKey
|
|
||||||
AWS_STORAGE_BUCKET_NAME:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: bucket
|
|
||||||
AWS_S3_REGION_NAME: local
|
|
||||||
OPENAI_API_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: OPENAI_API_KEY
|
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
|
||||||
WEBHOOK_API_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: WEBHOOK_API_TOKEN
|
|
||||||
WEBHOOK_URL: https://docs.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
|
|
||||||
CELERY_BROKER_URL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: redis-summary.redis.libre.sh
|
|
||||||
key: url
|
|
||||||
CELERY_RESULT_BACKEND:
|
|
||||||
secretKeyRef:
|
|
||||||
name: redis-summary.redis.libre.sh
|
|
||||||
key: url
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: lasuite/meet-summary
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "v0.1.12"
|
|
||||||
|
|
||||||
celery:
|
celery:
|
||||||
replicas: 1
|
replicas: 0
|
||||||
envVars:
|
|
||||||
APP_NAME: summary-microservice
|
|
||||||
APP_API_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: APP_API_TOKEN
|
|
||||||
AWS_S3_ENDPOINT_URL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: endpoint
|
|
||||||
AWS_S3_ACCESS_KEY_ID:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: accessKey
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: secretKey
|
|
||||||
AWS_STORAGE_BUCKET_NAME:
|
|
||||||
secretKeyRef:
|
|
||||||
name: meet-media-storage.bucket.libre.sh
|
|
||||||
key: bucket
|
|
||||||
AWS_S3_REGION_NAME: local
|
|
||||||
OPENAI_API_KEY:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: OPENAI_API_KEY
|
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
|
||||||
WEBHOOK_API_TOKEN:
|
|
||||||
secretKeyRef:
|
|
||||||
name: summary
|
|
||||||
key: WEBHOOK_API_TOKEN
|
|
||||||
WEBHOOK_URL: https://docs.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
|
|
||||||
CELERY_BROKER_URL:
|
|
||||||
secretKeyRef:
|
|
||||||
name: redis-summary.redis.libre.sh
|
|
||||||
key: url
|
|
||||||
CELERY_RESULT_BACKEND:
|
|
||||||
secretKeyRef:
|
|
||||||
name: redis-summary.redis.libre.sh
|
|
||||||
key: url
|
|
||||||
SENTRY_IS_ENABLED: True
|
|
||||||
SENTRY_DSN: https://5aead03f03505da5130af6d642c42faf@sentry.incubateur.net/202
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: lasuite/meet-summary
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "v0.1.12"
|
|
||||||
|
|
||||||
command:
|
|
||||||
- "celery"
|
|
||||||
- "-A"
|
|
||||||
- "summary.core.celery_worker"
|
|
||||||
- "worker"
|
|
||||||
- "--pool=solo"
|
|
||||||
- "--loglevel=info"
|
|
||||||
|
|||||||
@@ -216,14 +216,14 @@ summary:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: summary
|
name: summary
|
||||||
key: OPENAI_API_KEY
|
key: OPENAI_API_KEY
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
||||||
WEBHOOK_API_TOKEN:
|
WEBHOOK_API_TOKEN:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: summary
|
name: summary
|
||||||
key: WEBHOOK_API_TOKEN
|
key: WEBHOOK_API_TOKEN
|
||||||
WEBHOOK_URL: https://impress-staging.beta.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
|
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
||||||
CELERY_BROKER_URL:
|
CELERY_BROKER_URL:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: redis-summary.redis.libre.sh
|
name: redis-summary.redis.libre.sh
|
||||||
@@ -267,14 +267,11 @@ celery:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: summary
|
name: summary
|
||||||
key: OPENAI_API_KEY
|
key: OPENAI_API_KEY
|
||||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
|
||||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
|
||||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
|
||||||
WEBHOOK_API_TOKEN:
|
WEBHOOK_API_TOKEN:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: summary
|
name: summary
|
||||||
key: WEBHOOK_API_TOKEN
|
key: WEBHOOK_API_TOKEN
|
||||||
WEBHOOK_URL: https://impress-staging.beta.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
|
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
||||||
CELERY_BROKER_URL:
|
CELERY_BROKER_URL:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: redis-summary.redis.libre.sh
|
name: redis-summary.redis.libre.sh
|
||||||
|
|||||||
+6
-60
@@ -1,8 +1,4 @@
|
|||||||
environments:
|
environments:
|
||||||
dev-keycloak:
|
|
||||||
values:
|
|
||||||
- version: 0.0.1
|
|
||||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
|
||||||
dev:
|
dev:
|
||||||
values:
|
values:
|
||||||
- version: 0.0.1
|
- version: 0.0.1
|
||||||
@@ -36,8 +32,7 @@ repositories:
|
|||||||
|
|
||||||
releases:
|
releases:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
chart: bitnami/postgresql
|
chart: bitnami/postgresql
|
||||||
version: 13.1.5
|
version: 13.1.5
|
||||||
@@ -50,50 +45,9 @@ releases:
|
|||||||
enabled: true
|
enabled: true
|
||||||
autoGenerated: true
|
autoGenerated: true
|
||||||
|
|
||||||
- name: keycloak
|
|
||||||
installed: {{ eq .Environment.Name "dev-keycloak" | toYaml }}
|
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
|
||||||
chart: bitnami/keycloak
|
|
||||||
version: 17.3.6
|
|
||||||
values:
|
|
||||||
- postgresql:
|
|
||||||
auth:
|
|
||||||
username: keycloak
|
|
||||||
password: keycloak
|
|
||||||
database: keycloak
|
|
||||||
- extraEnvVars:
|
|
||||||
- name: KEYCLOAK_EXTRA_ARGS
|
|
||||||
value: "--import-realm"
|
|
||||||
- name: KC_HOSTNAME_URL
|
|
||||||
value: https://keycloak.127.0.0.1.nip.io
|
|
||||||
- extraVolumes:
|
|
||||||
- name: import
|
|
||||||
configMap:
|
|
||||||
name: meet-keycloak
|
|
||||||
- extraVolumeMounts:
|
|
||||||
- name: import
|
|
||||||
mountPath: /opt/bitnami/keycloak/data/import/
|
|
||||||
- auth:
|
|
||||||
adminUser: su
|
|
||||||
adminPassword: su
|
|
||||||
- proxy: edge
|
|
||||||
- ingress:
|
|
||||||
enabled: true
|
|
||||||
hostname: keycloak.127.0.0.1.nip.io
|
|
||||||
- extraDeploy:
|
|
||||||
- apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: meet-keycloak
|
|
||||||
data:
|
|
||||||
meet.json: |
|
|
||||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 14 }}
|
|
||||||
|
|
||||||
- name: minio
|
- name: minio
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
missingFileHandler: Warn
|
|
||||||
chart: bitnami/minio
|
chart: bitnami/minio
|
||||||
version: 12.10.10
|
version: 12.10.10
|
||||||
values:
|
values:
|
||||||
@@ -121,8 +75,7 @@ releases:
|
|||||||
name: mkcert
|
name: mkcert
|
||||||
|
|
||||||
- name: redis
|
- name: redis
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
chart: bitnami/redis
|
chart: bitnami/redis
|
||||||
version: 18.19.2
|
version: 18.19.2
|
||||||
@@ -132,8 +85,7 @@ releases:
|
|||||||
architecture: standalone
|
architecture: standalone
|
||||||
|
|
||||||
- name: extra
|
- name: extra
|
||||||
installed: {{ not (regexMatch "^dev.*" .Environment.Name) | toYaml }}
|
installed: {{ ne .Environment.Name "dev" | toYaml }}
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
chart: ./extra
|
chart: ./extra
|
||||||
secrets:
|
secrets:
|
||||||
@@ -148,32 +100,26 @@ releases:
|
|||||||
- name: meet
|
- name: meet
|
||||||
version: {{ .Values.version }}
|
version: {{ .Values.version }}
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
missingFileHandler: Warn
|
|
||||||
chart: ./meet
|
chart: ./meet
|
||||||
values:
|
values:
|
||||||
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
|
||||||
secrets:
|
secrets:
|
||||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||||
|
|
||||||
- name: livekit
|
- name: livekit
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
chart: livekit/livekit-server
|
chart: livekit/livekit-server
|
||||||
values:
|
values:
|
||||||
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
|
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
|
||||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
|
||||||
secrets:
|
secrets:
|
||||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||||
|
|
||||||
- name: livekit-egress
|
- name: livekit-egress
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||||
missingFileHandler: Warn
|
|
||||||
namespace: {{ .Namespace }}
|
namespace: {{ .Namespace }}
|
||||||
chart: livekit/egress
|
chart: livekit/egress
|
||||||
values:
|
values:
|
||||||
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
|
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
|
||||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
|
||||||
secrets:
|
secrets:
|
||||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||||
|
|||||||
Generated
+4
-5
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.5.4",
|
"@html-to/text-cli": "0.5.4",
|
||||||
@@ -344,9 +344,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"license": "MIT",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
"shebang-command": "^2.0.0",
|
"shebang-command": "^2.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "0.1.12",
|
"version": "0.1.10",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ dependencies = [
|
|||||||
"pydantic>=2.5.0",
|
"pydantic>=2.5.0",
|
||||||
"pydantic-settings>=2.1.0",
|
"pydantic-settings>=2.1.0",
|
||||||
"celery==5.4.0",
|
"celery==5.4.0",
|
||||||
"redis==5.2.1",
|
"redis==4.5.4",
|
||||||
"minio==7.2.13",
|
"minio==7.2.9",
|
||||||
"openai==1.58.1",
|
"openai==1.55.3",
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
"sentry-sdk[fastapi, celery]==2.19.0",
|
"sentry-sdk[fastapi, celery]==2.19.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff==0.8.4",
|
"ruff==0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -125,10 +125,8 @@ def process_audio_transcribe_summarize(filename: str, email: str, sub: str):
|
|||||||
summary = summary_response.choices[0].message.content
|
summary = summary_response.choices[0].message.content
|
||||||
logger.debug("Summary: \n %s", summary)
|
logger.debug("Summary: \n %s", summary)
|
||||||
|
|
||||||
# fixme - generate a title using LLM
|
|
||||||
data = {
|
data = {
|
||||||
"title": "Votre résumé",
|
"summary": summary,
|
||||||
"content": summary,
|
|
||||||
"email": email,
|
"email": email,
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user