Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine fdb5d447b0 🩹(backend) identify externally provisioned users to PostHog
In the external API, applications authenticate with a
client_id/secret and can provision users with only an email. In that
flow, users are not identified to PostHog, so the user DB id alone
is not enough to identify them in analytics afterwards.

The issue does not exist in the public API, where users are
authenticated and therefore already identified.

Inspired by @flochehab's approach in summary.
2026-07-10 00:07:36 +02:00
48 changed files with 260 additions and 1272 deletions
-20
View File
@@ -8,33 +8,14 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Added
- ✨(backend) allow searching the recording admin table by owner email
- ✨(frontend) add participant color gradient when camera is off #1490
- ✨(all) allow forcing SSO display name for authenticated users
- (frontend) install vite-plugin-static-copy for MediaPipe WASM assets
- ✨(addon) show add-in tools when creating meetings in shared calendars
### Changed ### Changed
- 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1 - 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
- ⬆️(mail) update mjml to v5 and @html-to/text-cli - ⬆️(mail) update mjml to v5 and @html-to/text-cli
- 🚸(frontend) initialize the join input name with the persisted full name
- ♻️(frontend) refactor background processors to use the new API
- ♻️(frontend) inline model weights to avoid loading them from remote
- ♻️(frontend) inline MediaPipe WASM modules to avoid loading from remote
- ⬆️(frontend) upgrade posthog-js from 1.387.0 to 1.391.2
- ⬆️(frontend) upgrade react-stately from 3.47.0 to 3.48.0
- ⬆️(frontend) upgrade react-aria from 3.49.0 to 3.50.0
- ⬆️(frontend) upgrade react-aria-components from 1.18.0 to 1.19.0
### Fixed ### Fixed
- 🩹(backend) identify externally provisioned users to PostHog - 🩹(backend) identify externally provisioned users to PostHog
- 🐛(backend) fix info panel crash for unregistered rooms
- ♿️(frontend) focus side panel container on open #1452
- 🐛(summary) whisper call error handling
## [1.23.0] - 2026-07-08 ## [1.23.0] - 2026-07-08
@@ -78,7 +59,6 @@ and this project adheres to
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks - ✨(backend) add fallback to save recordings without S3/MinIO webhooks
- 🩹(frontend) enable screen share button in PiP #1458 - 🩹(frontend) enable screen share button in PiP #1458
- 🐛(backend) support unencoded S3 notification object keys #1455 - 🐛(backend) support unencoded S3 notification object keys #1455
- ✨(frontend) prioritize screen share in picture-in-picture layout #1467
### Changed ### Changed
+1 -1
View File
@@ -37,7 +37,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev uv sync --locked --no-dev
# ---- mails ---- # ---- mails ----
FROM node:22 AS mail-builder FROM node:20 AS mail-builder
COPY ./src/mail /mail/app COPY ./src/mail /mail/app
+4 -4
View File
@@ -11,14 +11,14 @@ There are two ways to customize LaSuite Meet:
### How to Use ### How to Use
To use this feature, simply set the `FRONTEND_CUSTOM_CSS_URL` environment variable (of the **backend** service) to the URL of your custom CSS file. For example: To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. For example:
```javascript ```javascript
FRONTEND_CUSTOM_CSS_URL=https://example.com/custom-style.css FRONTEND_CSS_URL=https://example.com/custom-style.css
``` ```
> [!TIP] > [!TIP]
> If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CUSTOM_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`. > If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`.
Setting this variable makes the app load your CSS at runtime, adding a `<link>` to `<head>` so you can override CSS variables and customize the frontend without rebuilding. Setting this variable makes the app load your CSS at runtime, adding a `<link>` to `<head>` so you can override CSS variables and customize the frontend without rebuilding.
@@ -37,7 +37,7 @@ Let's say you want to change the font of our application to a custom font. You c
} }
``` ```
Then, set the `FRONTEND_CUSTOM_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified. Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified.
> [!IMPORTANT] > [!IMPORTANT]
> You can override any CSS token—semantic or palette. See [panda.config.ts](../src/frontend/panda.config.ts) for all defined semantic tokens. > You can override any CSS token—semantic or palette. See [panda.config.ts](../src/frontend/panda.config.ts) for all defined semantic tokens.
+1 -170
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp"> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id> <Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id>
<Version>1.0.0.0</Version> <Version>0.0.2.0</Version>
<ProviderName>__APP_NAME__</ProviderName> <ProviderName>__APP_NAME__</ProviderName>
<DefaultLocale>fr-FR</DefaultLocale> <DefaultLocale>fr-FR</DefaultLocale>
<DisplayName DefaultValue="__APP_NAME__"/> <DisplayName DefaultValue="__APP_NAME__"/>
@@ -205,174 +205,5 @@
</bt:String> </bt:String>
</bt:LongStrings> </bt:LongStrings>
</Resources> </Resources>
<!-- ─── V1.1 override: required for shared folder / delegate support ─── -->
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
<Requirements>
<bt:Sets DefaultMinVersion="1.8">
<bt:Set Name="Mailbox"/>
</bt:Sets>
</Requirements>
<Hosts>
<Host xsi:type="MailHost">
<DesktopFormFactor>
<FunctionFile resid="Commands.Url"/>
<SupportsSharedFolders>true</SupportsSharedFolders>
<!-- ─── Mail: Read ─────────────────────────────────────────── -->
<ExtensionPoint xsi:type="MessageReadCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgReadGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgReadOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- ─── Mail: Compose ─────────────────────────────────────── -->
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgComposeGenerateLinkButton">
<Label resid="GenerateLink.Label"/>
<Supertip>
<Title resid="GenerateLink.Label"/>
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromMail</FunctionName>
</Action>
</Control>
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Settings.16x16"/>
<bt:Image size="32" resid="Settings.32x32"/>
<bt:Image size="80" resid="Settings.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- ─── Calendar: Compose (New/Edit appointment) ──────────── -->
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
<OfficeTab id="TabDefault">
<Group id="apptComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="apptGenerateLinkButton">
<Label resid="GenerateLink.Label"/>
<Supertip>
<Title resid="GenerateLink.Label"/>
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
</Action>
</Control>
<Control xsi:type="Button" id="apptOpenSettingsButton">
<Label resid="OpenSettings.Label"/>
<Supertip>
<Title resid="OpenSettings.Label"/>
<Description resid="OpenSettings.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Settings.16x16"/>
<bt:Image size="32" resid="Settings.32x32"/>
<bt:Image size="80" resid="Settings.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Settings.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-16.png"/>
<bt:Image id="Settings.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-32.png"/>
<bt:Image id="Settings.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-80.png"/>
<bt:Image id="Add.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/add-16.png"/>
<bt:Image id="Add.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/add-32.png"/>
<bt:Image id="Add.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/add-80.png"/>
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/addons/outlook/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/addons/outlook/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<!-- Default (French) -->
<bt:String id="GroupLabel" DefaultValue="__APP_NAME__"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__">
<bt:Override Locale="en-US" Value="Add a __APP_NAME__ link"/>
<bt:Override Locale="de-DE" Value="__APP_NAME__-Link hinzufügen"/>
</bt:String>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
<bt:Override Locale="en-US" Value="Open settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
</bt:String>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
<bt:Override Locale="en-US" Value="Settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen"/>
</bt:String>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement.">
<bt:Override Locale="de-DE" Value="Generiert einen __APP_NAME__-Besprechungslink und fügt ihn in den Termin ein."/>
<bt:Override Locale="en-US" Value="Generates a __APP_NAME__ meeting link and inserts it into the item."/>
</bt:String>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
</bt:String>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
</bt:String>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</VersionOverrides> </VersionOverrides>
</OfficeApp> </OfficeApp>
@@ -57,7 +57,8 @@
data-i18n="footer.feedback" data-i18n="footer.feedback"
></a> ></a>
<div id="footer-right"> <div id="footer-right">
<span class="version-number">1.0.0</span> <span class="version-badge">beta</span>
<span class="version-number">0.0.2</span>
</div> </div>
</footer> </footer>
</body> </body>
+1 -8
View File
@@ -402,14 +402,7 @@ class RecordingAdmin(admin.ModelAdmin):
"""Recording admin interface declaration.""" """Recording admin interface declaration."""
inlines = (RecordingAccessInline,) inlines = (RecordingAccessInline,)
search_fields = [ search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
"status",
"=id",
"worker_id",
"room__slug",
"=room__id",
"accesses__user__email",
]
list_display = ( list_display = (
"id", "id",
"status", "status",
-3
View File
@@ -68,9 +68,6 @@ def get_frontend_configuration(request):
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND, "enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
"default_sources": settings.LIVEKIT_DEFAULT_SOURCES, "default_sources": settings.LIVEKIT_DEFAULT_SOURCES,
}, },
"authenticated_users_can_edit_display_name": (
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
),
} }
frontend_configuration.update(settings.FRONTEND_CONFIGURATION) frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
return Response(frontend_configuration) return Response(frontend_configuration)
-4
View File
@@ -92,7 +92,6 @@ from core.services.subtitle import SubtitleException, SubtitleService
from core.tasks.file import process_file_deletion from core.tasks.file import process_file_deletion
from ..authentication.livekit import LiveKitTokenAuthentication from ..authentication.livekit import LiveKitTokenAuthentication
from ..models import RoomAccessLevel
from . import permissions, serializers, throttling from . import permissions, serializers, throttling
from .feature_flag import FeatureFlag from .feature_flag import FeatureFlag
@@ -268,9 +267,6 @@ class RoomViewSet(
username = request.query_params.get("username", None) username = request.query_params.get("username", None)
data = { data = {
"id": None, "id": None,
"slug": slug,
"is_administrable": False,
"access_level": RoomAccessLevel.PUBLIC,
"livekit": { "livekit": {
"url": settings.LIVEKIT_CONFIGURATION["url"], "url": settings.LIVEKIT_CONFIGURATION["url"],
"room": slug, "room": slug,
@@ -130,9 +130,6 @@ def test_api_rooms_retrieve_anonymous_unregistered_allowed(mock_token):
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"id": None, "id": None,
"slug": "unregistered-room",
"access_level": "public",
"is_administrable": False,
"livekit": { "livekit": {
"url": "test_url_value", "url": "test_url_value",
"room": "unregistered-room", "room": "unregistered-room",
@@ -165,9 +162,6 @@ def test_api_rooms_retrieve_anonymous_unregistered_allowed_not_normalized(mock_t
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"id": None, "id": None,
"slug": "reunion",
"access_level": "public",
"is_administrable": False,
"livekit": { "livekit": {
"url": "test_url_value", "url": "test_url_value",
"room": "reunion", "room": "reunion",
+1 -97
View File
@@ -2,109 +2,13 @@
Test utils functions Test utils functions
""" """
# pylint: disable=W0621
import json import json
from unittest import mock from unittest import mock
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
import jwt
import pytest import pytest
from livekit.api import TwirpError from livekit.api import TwirpError
from core.factories import UserFactory from core.utils import NotificationError, create_livekit_client, notify_participants
from core.utils import (
NotificationError,
create_livekit_client,
generate_token,
notify_participants,
)
pytestmark = pytest.mark.django_db
def decode_token(token: str) -> dict:
"""Decode a LiveKit JWT access token for inspection."""
return jwt.decode(
token,
settings.LIVEKIT_CONFIGURATION["api_secret"],
algorithms=["HS256"],
)
def test_generate_token_authenticated_uses_full_name():
"""The token's display name should default to the user's full name."""
user = UserFactory(full_name="Jane Doe")
token = generate_token(room="my-room", user=user)
claims = decode_token(token)
assert claims["name"] == "Jane Doe"
assert claims["sub"] == str(user.sub)
def test_generate_token_authenticated_fallback_user_representation():
"""
When the user has no full name, the token's display name should fall back
to the user's string representation.
"""
user = UserFactory(full_name=None)
token = generate_token(room="my-room", user=user)
claims = decode_token(token)
assert claims["name"] == str(user)
def test_generate_token_explicit_username_overrides_default():
"""An explicitly provided username should take precedence over the full name."""
user = UserFactory(full_name="Jane Doe")
token = generate_token(room="my-room", user=user, username="Custom Name")
claims = decode_token(token)
assert claims["name"] == "Custom Name"
def test_authenticated_username_ignored_when_editing_disabled(settings):
"""With editing disabled, an authenticated user's username is ignored."""
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
user = UserFactory(full_name="Jane Doe")
token = generate_token(room="my-room", user=user, username="Custom Name")
claims = decode_token(token)
assert claims["name"] == "Jane Doe"
def test_authenticated_default_name_unaffected_when_editing_disabled(settings):
"""Disabling editing doesn't disturb the default full-name path."""
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
user = UserFactory(full_name="Jane Doe")
token = generate_token(room="my-room", user=user)
claims = decode_token(token)
assert claims["name"] == "Jane Doe"
def test_anonymous_uses_username_when_provided():
"""An anonymous user's provided username is used as the display name."""
token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
claims = decode_token(token)
assert claims["name"] == "Guest42"
def test_anonymous_username_used_even_when_editing_disabled(settings):
"""The setting governs authenticated users only; anonymous can still set a name."""
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
claims = decode_token(token)
assert claims["name"] == "Guest42"
def test_anonymous_falls_back_to_anonymous_label():
"""With no username, an anonymous user is labelled 'Anonymous'."""
token = generate_token(room="my-room", user=AnonymousUser())
claims = decode_token(token)
assert claims["name"] == "Anonymous"
@mock.patch("asyncio.get_running_loop") @mock.patch("asyncio.get_running_loop")
+2 -7
View File
@@ -109,16 +109,11 @@ def generate_token(
default_username = "Anonymous" default_username = "Anonymous"
else: else:
identity = str(user.sub) identity = str(user.sub)
default_username = user.full_name or str(user) default_username = str(user)
if color is None: if color is None:
color = generate_color(identity) color = generate_color(identity)
can_edit = (
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME or user.is_anonymous
)
display_name = (username or default_username) if can_edit else default_username
token = ( token = (
AccessToken( AccessToken(
api_key=settings.LIVEKIT_CONFIGURATION["api_key"], api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
@@ -126,7 +121,7 @@ def generate_token(
) )
.with_grants(video_grants) .with_grants(video_grants)
.with_identity(identity) .with_identity(identity)
.with_name(display_name) .with_name(username or default_username)
.with_attributes( .with_attributes(
{"color": color, "room_admin": "true" if is_admin_or_owner else "false"} {"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
) )
-5
View File
@@ -671,11 +671,6 @@ class Base(Configuration):
environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS", environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS",
environ_prefix=None, environ_prefix=None,
) )
AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = values.BooleanValue(
True,
environ_name="AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME",
environ_prefix=None,
)
# Recording settings # Recording settings
RECORDING_ENABLE = values.BooleanValue( RECORDING_ENABLE = values.BooleanValue(
+35 -214
View File
@@ -15,7 +15,6 @@
"@livekit/components-react": "2.9.21", "@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3", "@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0", "@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
@@ -30,13 +29,13 @@
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.19.2", "livekit-client": "2.19.2",
"posthog-js": "1.391.2", "posthog-js": "1.387.0",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.49.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.18.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-i18next": "17.0.8", "react-i18next": "17.0.8",
"react-stately": "3.48.0", "react-stately": "3.47.0",
"use-sound": "5.0.0", "use-sound": "5.0.0",
"valtio": "2.3.2", "valtio": "2.3.2",
"wouter": "3.10.0" "wouter": "3.10.0"
@@ -64,7 +63,6 @@
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.60.1", "typescript-eslint": "8.60.1",
"vite": "8.0.14", "vite": "8.0.14",
"vite-plugin-static-copy": "4.1.1",
"vite-plugin-svgr": "5.2.0" "vite-plugin-svgr": "5.2.0"
} }
}, },
@@ -112,62 +110,6 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/@adobe/react-spectrum/node_modules/react-aria": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.49.0.tgz",
"integrity": "sha512-4+oK9FwJQWYhyA5zLfj/feOGY0zZbkE1muoF4gyxMroHVypjcYaRSTlJwvxph2zIlxt757KX6xIK2wJ5Aw1Kog==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.2",
"@internationalized/number": "^3.6.7",
"@internationalized/string": "^3.2.9",
"@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0",
"aria-hidden": "^1.2.3",
"clsx": "^2.0.0",
"react-stately": "3.47.0",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@adobe/react-spectrum/node_modules/react-aria-components": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.18.0.tgz",
"integrity": "sha512-FhRQjuDkH4WhgFv+O2sYTzK3JzdZTGpBeaqfRlfTo+DcSZzD8elJEkytHe7SDpcexVKeire8NVd7OruZHfCVoA==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.2",
"@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0",
"client-only": "^0.0.1",
"react-aria": "3.49.0",
"react-stately": "3.47.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@adobe/react-spectrum/node_modules/react-stately": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.47.0.tgz",
"integrity": "sha512-H3ar+SOWP920EbVg7qWfP3fZjZiwhlEJAEJQqjt+w8oKijCwFgr0+R4941PIHscOXRNRvEOjvWilitImC0DdBg==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.2",
"@internationalized/number": "^3.6.7",
"@internationalized/string": "^3.2.9",
"@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -1810,9 +1752,9 @@
} }
}, },
"node_modules/@react-types/shared": { "node_modules/@react-types/shared": {
"version": "3.36.0", "version": "3.35.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.0.tgz", "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.35.0.tgz",
"integrity": "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==", "integrity": "sha512-iNWvuzEwANttpQpdlu8nPBtdHb0mcCMj1ZTH//iRB5E/14IAnyRlR25rxH7pNLyzHINsPGEKnWvpwDMCT6vziQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -3250,19 +3192,6 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bl": { "node_modules/bl": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
@@ -7165,19 +7094,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-boolean-object": { "node_modules/is-boolean-object": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
@@ -8759,19 +8675,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-map": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz",
"integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-manager-detector": { "node_modules/package-manager-detector": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
@@ -9130,19 +9033,19 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.391.2", "version": "1.387.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.391.2.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.387.0.tgz",
"integrity": "sha512-q0DZN6ljchSnAFJIXf+sQFTPlsLjTlRa+TvrL+QRb6413BGtib/MNiQy1bnwLKt8KR+f6xJYvkqdLyty9s4Aww==", "integrity": "sha512-Pv1jUMySMN62zoAxdJBJPV8n62lkHdjuWhpeU7izczc5Dqbx3hhqO2hkrNTI8Yx1ezmWk2qUHZs03FuOBubdFQ==",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@posthog/core": "^1.35.3", "@posthog/core": "^1.33.0",
"@posthog/types": "^1.390.2", "@posthog/types": "^1.387.0",
"core-js": "^3.38.1", "core-js": "^3.38.1",
"dompurify": "^3.3.2", "dompurify": "^3.3.2",
"fflate": "^0.4.8", "fflate": "^0.4.8",
"preact": "^10.29.2", "preact": "^10.28.2",
"query-selector-shadow-dom": "^1.0.1", "query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.3.0" "web-vitals": "^5.1.0"
} }
}, },
"node_modules/powershell-utils": { "node_modules/powershell-utils": {
@@ -9159,21 +9062,13 @@
} }
}, },
"node_modules/preact": { "node_modules/preact": {
"version": "10.29.7", "version": "10.28.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
"integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==", "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/preact" "url": "https://opencollective.com/preact"
},
"peerDependencies": {
"preact-render-to-string": ">=5"
},
"peerDependenciesMeta": {
"preact-render-to-string": {
"optional": true
}
} }
}, },
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
@@ -9380,19 +9275,19 @@
} }
}, },
"node_modules/react-aria": { "node_modules/react-aria": {
"version": "3.50.0", "version": "3.49.0",
"resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.50.0.tgz", "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.49.0.tgz",
"integrity": "sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==", "integrity": "sha512-4+oK9FwJQWYhyA5zLfj/feOGY0zZbkE1muoF4gyxMroHVypjcYaRSTlJwvxph2zIlxt757KX6xIK2wJ5Aw1Kog==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@internationalized/date": "^3.12.2", "@internationalized/date": "^3.12.2",
"@internationalized/number": "^3.6.7", "@internationalized/number": "^3.6.7",
"@internationalized/string": "^3.2.9", "@internationalized/string": "^3.2.9",
"@react-types/shared": "^3.36.0", "@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"aria-hidden": "^1.2.3", "aria-hidden": "^1.2.3",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"react-stately": "3.48.0", "react-stately": "3.47.0",
"use-sync-external-store": "^1.6.0" "use-sync-external-store": "^1.6.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -9401,17 +9296,17 @@
} }
}, },
"node_modules/react-aria-components": { "node_modules/react-aria-components": {
"version": "1.19.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.19.0.tgz", "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.18.0.tgz",
"integrity": "sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==", "integrity": "sha512-FhRQjuDkH4WhgFv+O2sYTzK3JzdZTGpBeaqfRlfTo+DcSZzD8elJEkytHe7SDpcexVKeire8NVd7OruZHfCVoA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@internationalized/date": "^3.12.2", "@internationalized/date": "^3.12.2",
"@react-types/shared": "^3.36.0", "@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"client-only": "^0.0.1", "client-only": "^0.0.1",
"react-aria": "3.50.0", "react-aria": "3.49.0",
"react-stately": "3.48.0" "react-stately": "3.47.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
@@ -9465,15 +9360,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-stately": { "node_modules/react-stately": {
"version": "3.48.0", "version": "3.47.0",
"resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.48.0.tgz", "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.47.0.tgz",
"integrity": "sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==", "integrity": "sha512-H3ar+SOWP920EbVg7qWfP3fZjZiwhlEJAEJQqjt+w8oKijCwFgr0+R4941PIHscOXRNRvEOjvWilitImC0DdBg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@internationalized/date": "^3.12.2", "@internationalized/date": "^3.12.2",
"@internationalized/number": "^3.6.7", "@internationalized/number": "^3.6.7",
"@internationalized/string": "^3.2.9", "@internationalized/string": "^3.2.9",
"@react-types/shared": "^3.36.0", "@react-types/shared": "^3.35.0",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.6.0" "use-sync-external-store": "^1.6.0"
}, },
@@ -11242,80 +11137,6 @@
} }
} }
}, },
"node_modules/vite-plugin-static-copy": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-4.1.1.tgz",
"integrity": "sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"p-map": "^7.0.4",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.17"
},
"engines": {
"node": "^22.0.0 || >=24.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/sapphi-red"
},
"peerDependencies": {
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/vite-plugin-static-copy/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/vite-plugin-static-copy/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/vite-plugin-static-copy/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/vite-plugin-svgr": { "node_modules/vite-plugin-svgr": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz",
@@ -11649,9 +11470,9 @@
} }
}, },
"node_modules/web-vitals": { "node_modules/web-vitals": {
"version": "5.3.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
"integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/webrtc-adapter": { "node_modules/webrtc-adapter": {
+5 -7
View File
@@ -17,12 +17,11 @@
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6", "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4", "@libreaudio/la-call": "0.1.4",
"@fontsource/opendyslexic": "5.2.5",
"@livekit/components-react": "2.9.21", "@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3", "@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0", "@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
@@ -37,13 +36,13 @@
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.19.2", "livekit-client": "2.19.2",
"posthog-js": "1.391.2", "posthog-js": "1.387.0",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.49.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.18.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-i18next": "17.0.8", "react-i18next": "17.0.8",
"react-stately": "3.48.0", "react-stately": "3.47.0",
"use-sound": "5.0.0", "use-sound": "5.0.0",
"valtio": "2.3.2", "valtio": "2.3.2",
"wouter": "3.10.0" "wouter": "3.10.0"
@@ -71,7 +70,6 @@
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.60.1", "typescript-eslint": "8.60.1",
"vite": "8.0.14", "vite": "8.0.14",
"vite-plugin-static-copy": "4.1.1",
"vite-plugin-svgr": "5.2.0" "vite-plugin-svgr": "5.2.0"
} }
} }
-1
View File
@@ -58,7 +58,6 @@ export interface ApiConfig {
transcription_destination?: string transcription_destination?: string
max_participants_for_sound: number max_participants_for_sound: number
auto_mute_on_join_threshold: number auto_mute_on_join_threshold: number
authenticated_users_can_edit_display_name: boolean
} }
const fetchConfig = (): Promise<ApiConfig> => { const fetchConfig = (): Promise<ApiConfig> => {
@@ -9,11 +9,10 @@ import { useState } from 'react'
import { menuRecipe } from '@/primitives/menuRecipe' import { menuRecipe } from '@/primitives/menuRecipe'
import { ApiRoom } from '@/features/rooms/api/ApiRoom' import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { useSnapshot } from 'valtio' import { loadUserChoices } from '@livekit/components-core'
import { userStore } from '@/stores/user'
export const CreateMeetingMenu = () => { export const CreateMeetingMenu = () => {
const { username } = useSnapshot(userStore) const { username } = loadUserChoices()
const { t } = useTranslation('home') const { t } = useTranslation('home')
const { mutateAsync: createRoom } = useCreateRoom() const { mutateAsync: createRoom } = useCreateRoom()
@@ -26,7 +26,6 @@ export const PipFocusLayout = memo(
<ParticipantTile <ParticipantTile
key={getTrackKey(mainTrack)} key={getTrackKey(mainTrack)}
trackRef={mainTrack} trackRef={mainTrack}
disableTileControls
/> />
</MainSlot> </MainSlot>
)} )}
@@ -35,7 +34,6 @@ export const PipFocusLayout = memo(
<ParticipantTile <ParticipantTile
key={getTrackKey(thumbnailTrack)} key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack} trackRef={thumbnailTrack}
disableTileControls
/> />
</Thumbnail> </Thumbnail>
)} )}
@@ -50,10 +48,9 @@ const FocusContainer = styled('div', {
position: 'relative', position: 'relative',
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '8px', borderRadius: '4px',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: 'primaryDark.100', backgroundColor: 'primaryDark.100',
boxSizing: 'border-box',
}, },
}) })
@@ -61,8 +58,6 @@ const MainSlot = styled('div', {
base: { base: {
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '8px',
overflow: 'hidden',
'& .lk-participant-media-video': { '& .lk-participant-media-video': {
objectFit: 'contain', objectFit: 'contain',
}, },
@@ -72,13 +67,13 @@ const MainSlot = styled('div', {
const Thumbnail = styled('div', { const Thumbnail = styled('div', {
base: { base: {
position: 'absolute', position: 'absolute',
right: '1.25rem', right: '1rem',
bottom: '1.25rem', bottom: '1rem',
width: '42%', width: '42%',
maxWidth: '220px', maxWidth: '220px',
minWidth: '140px', minWidth: '140px',
aspectRatio: '16 / 9', aspectRatio: '16 / 9',
borderRadius: '8px', borderRadius: '4px',
overflow: 'hidden', overflow: 'hidden',
boxShadow: 'md', boxShadow: 'md',
zIndex: 2, zIndex: 2,
@@ -41,7 +41,7 @@ export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
<GridContainer ref={containerRef} style={gridStyle}> <GridContainer ref={containerRef} style={gridStyle}>
{tracks.map((track, index) => ( {tracks.map((track, index) => (
<GridCell key={getTrackKey(track)} style={placements[index]}> <GridCell key={getTrackKey(track)} style={placements[index]}>
<ParticipantTile trackRef={track} disableTileControls /> <ParticipantTile trackRef={track} />
</GridCell> </GridCell>
))} ))}
</GridContainer> </GridContainer>
@@ -54,8 +54,7 @@ const GridContainer = styled('div', {
width: '100%', width: '100%',
height: '100%', height: '100%',
display: 'grid', display: 'grid',
gap: '0.5rem', gap: '0.25rem',
boxSizing: 'border-box',
}, },
}) })
@@ -64,7 +63,7 @@ const GridCell = styled('div', {
position: 'relative', position: 'relative',
minWidth: 0, minWidth: 0,
minHeight: 0, minHeight: 0,
borderRadius: '8px', borderRadius: '4px',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: 'primaryDark.100', backgroundColor: 'primaryDark.100',
// Paint on own layer so FLIP transforms don't trigger layout thrash. // Paint on own layer so FLIP transforms don't trigger layout thrash.
@@ -1,97 +0,0 @@
import { memo } from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { styled } from '@/styled-system/jsx'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { getTrackKey } from '../../utils/pipTrackSelection'
type PipScreenShareLayoutProps = {
screenShareTrack: TrackReferenceOrPlaceholder
cameraTracks: TrackReferenceOrPlaceholder[]
}
/**
* Layout when a screen share is active.
* Camera tiles are shown as a compact row at the top; the screen share occupies
* the remaining space below, much larger than the camera tiles.
*/
export const PipScreenShareLayout = memo(
({ screenShareTrack, cameraTracks }: PipScreenShareLayoutProps) => {
return (
<LayoutContainer>
{cameraTracks.length > 0 && (
<CameraTilesRow>
{cameraTracks.map((track) => (
<CameraTile key={getTrackKey(track)}>
<ParticipantTile trackRef={track} disableTileControls />
</CameraTile>
))}
</CameraTilesRow>
)}
<ScreenShareSlot>
<ParticipantTile trackRef={screenShareTrack} disableTileControls />
</ScreenShareSlot>
</LayoutContainer>
)
}
)
PipScreenShareLayout.displayName = 'PipScreenShareLayout'
const LayoutContainer = styled('div', {
base: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
boxSizing: 'border-box',
overflow: 'hidden',
},
})
const CameraTilesRow = styled('div', {
base: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
gap: '0.5rem',
flexShrink: 0,
height: '22%',
minHeight: '60px',
maxHeight: '120px',
},
})
const CameraTile = styled('div', {
base: {
position: 'relative',
flex: '0 1 auto',
height: '100%',
aspectRatio: '16 / 9',
minWidth: 0,
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
'& .lk-participant-tile': {
width: '100%',
height: '100%',
},
},
})
const ScreenShareSlot = styled('div', {
base: {
position: 'relative',
flex: 1,
minHeight: 0,
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
'& .lk-participant-tile': {
width: '100%',
height: '100%',
},
'& .lk-participant-media-video': {
objectFit: 'contain',
},
},
})
@@ -1,11 +1,10 @@
import React, { useMemo } from 'react' import { useMemo } from 'react'
import { usePagination, useTracks } from '@livekit/components-react' import { usePagination, useTracks } from '@livekit/components-react'
import { RoomEvent, Track } from 'livekit-client' import { RoomEvent, Track } from 'livekit-client'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { PipFocusLayout } from './PipFocusLayout' import { PipFocusLayout } from './PipFocusLayout'
import { PipGridLayout } from './PipGridLayout' import { PipGridLayout } from './PipGridLayout'
import { PipPagination } from './PipPagination' import { PipPagination } from './PipPagination'
import { PipScreenShareLayout } from './PipScreenShareLayout'
import { StageFrame } from './StageFrame' import { StageFrame } from './StageFrame'
import { MAX_PIP_TILES } from '../../utils/pipGrid' import { MAX_PIP_TILES } from '../../utils/pipGrid'
import { import {
@@ -14,12 +13,9 @@ import {
} from '@livekit/components-core' } from '@livekit/components-core'
/** /**
* PipStage picks between three layouts: * PipStage picks between two layouts based on track count:
* - Screen share mode (any screen share active): * - Focus mode (≤ 2 tracks): one main track + one thumbnail overlay.
* small camera tiles in a row at the top, large screen share below. * - Grid mode (3+ tracks): adaptive tiling.
* - Grid mode (3+ camera tracks, no screen share): adaptive tiling.
* - Focus mode (≤ 2 camera tracks, no screen share): one main track
* + one thumbnail overlay.
*/ */
export const PipStage = () => { export const PipStage = () => {
const tracks = useTracks( const tracks = useTracks(
@@ -45,55 +41,51 @@ export const PipStage = () => {
[tracks] [tracks]
) )
// Cap camera tiles in screen-share mode. Called unconditionally for hook rules. // Grid mode order: screen share leads, then cameras (already ordered by
const paginatedCameraTracks = usePagination(MAX_PIP_TILES - 1, cameraTracks) // active speaker via the `ActiveSpeakersChanged` update above).
const gridTracks = useMemo(
() =>
screenShareTrack ? [screenShareTrack, ...cameraTracks] : cameraTracks,
[screenShareTrack, cameraTracks]
)
// Cap the grid at MAX_PIP_TILES per page for the non-screenshare grid mode. // Cap the grid at MAX_PIP_TILES per page. `usePagination` keeps the visible
const pagination = usePagination(MAX_PIP_TILES, cameraTracks) // page visually stable (active/recent speakers stay put) via its internal
// `useVisualStableUpdate`. Called unconditionally to respect hook rules.
const pagination = usePagination(MAX_PIP_TILES, gridTracks)
if (tracks.length === 0) return null if (tracks.length === 0) return null
// Screen share active /**
if (screenShareTrack) { * The focus layout shows one main track with one thumbnail overlay,
// Solo presenter: screen share fills the area, camera as small thumbnail * so it can only fit 2 tracks. Beyond that we switch to the grid.
if (cameraTracks.length <= 1) { */
return ( if (gridTracks.length > 2) {
return (
<StageWrapper>
<StageFrame> <StageFrame>
<PipFocusLayout <PipGridLayout tracks={pagination.tracks} />
mainTrack={screenShareTrack}
thumbnailTrack={cameraTracks[0]}
/>
</StageFrame> </StageFrame>
) <PipPagination
} totalPageCount={pagination.totalPageCount}
// Multiple cameras: camera row at top, screen share below currentPage={pagination.currentPage}
return ( nextPage={pagination.nextPage}
<PaginatedStage pagination={paginatedCameraTracks}> prevPage={pagination.prevPage}
<PipScreenShareLayout
screenShareTrack={screenShareTrack}
cameraTracks={paginatedCameraTracks.tracks}
/> />
</PaginatedStage> </StageWrapper>
) )
} }
// 3+ camera tracks → adaptive grid
if (cameraTracks.length > 2) {
return (
<PaginatedStage pagination={pagination}>
<PipGridLayout tracks={pagination.tracks} />
</PaginatedStage>
)
}
// ≤ 2 camera tracks → focus layout (main + optional thumbnail)
const localCameraTrack = cameraTracks.find( const localCameraTrack = cameraTracks.find(
(track) => track.participant?.isLocal (track) => track.participant?.isLocal
) )
const remoteCameraTrack = cameraTracks.find( const remoteCameraTrack = cameraTracks.find(
(track) => !track.participant?.isLocal (track) => !track.participant?.isLocal
) )
const mainTrack = remoteCameraTrack ?? localCameraTrack
const mainTrack = screenShareTrack ?? remoteCameraTrack ?? localCameraTrack
const thumbnailTrack = const thumbnailTrack =
mainTrack === localCameraTrack ? undefined : localCameraTrack mainTrack === localCameraTrack ? undefined : localCameraTrack
@@ -104,31 +96,6 @@ export const PipStage = () => {
) )
} }
type PaginationResult = {
totalPageCount: number
currentPage: number
nextPage: () => void
prevPage: () => void
}
const PaginatedStage = ({
pagination,
children,
}: {
pagination: PaginationResult
children: React.ReactNode
}) => (
<StageWrapper>
<StageFrame>{children}</StageFrame>
<PipPagination
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
nextPage={pagination.nextPage}
prevPage={pagination.prevPage}
/>
</StageWrapper>
)
const StageWrapper = styled('div', { const StageWrapper = styled('div', {
base: { base: {
display: 'flex', display: 'flex',
@@ -25,8 +25,8 @@ const Container = styled('div', {
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
minHeight: 0, minHeight: 0,
padding: '0.5rem', marginLeft: '0.5rem',
boxSizing: 'border-box', marginRight: '0.5rem',
borderRadius: '4px', borderRadius: '4px',
overflow: 'hidden', overflow: 'hidden',
}, },
@@ -22,7 +22,7 @@ export type ApiRoom = {
id: string id: string
name: string name: string
slug: string slug: string
pin_code?: string pin_code: string
is_administrable: boolean is_administrable: boolean
access_level: ApiAccessLevel access_level: ApiAccessLevel
livekit?: ApiLiveKit livekit?: ApiLiveKit
@@ -36,7 +36,6 @@ import { PictureInPictureConference } from '@/features/pip/components/PictureInP
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils' import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { userPreferencesStore } from '@/stores/userPreferences' import { userPreferencesStore } from '@/stores/userPreferences'
import { userStore } from '@/stores/user'
export const Conference = ({ export const Conference = ({
roomId, roomId,
@@ -54,8 +53,6 @@ export const Conference = ({
userChoices: LocalUserChoices userChoices: LocalUserChoices
} }
const { username } = useSnapshot(userStore)
useEffect(() => { useEffect(() => {
posthog.capture('visit-room', { slug: roomId }) posthog.capture('visit-room', { slug: roomId })
}, [roomId, posthog]) }, [roomId, posthog])
@@ -86,10 +83,10 @@ export const Conference = ({
queryFn: () => queryFn: () =>
fetchRoom({ fetchRoom({
roomId: roomId as string, roomId: roomId as string,
username: username, username: userConfig.username,
}).catch((error) => { }).catch((error) => {
if (error.statusCode == '404') { if (error.statusCode == '404') {
createRoom({ slug: roomId, username }) createRoom({ slug: roomId, username: userConfig.username })
} }
}), }),
retry: false, retry: false,
@@ -42,17 +42,14 @@ import {
saveAudioInputDeviceId, saveAudioInputDeviceId,
saveAudioInputEnabled, saveAudioInputEnabled,
saveAudioOutputDeviceId, saveAudioOutputDeviceId,
saveUsername,
saveVideoInputDeviceId, saveVideoInputDeviceId,
saveVideoInputEnabled, saveVideoInputEnabled,
userChoicesStore, userChoicesStore,
} from '@/stores/userChoices' } from '@/stores/userChoices'
import { saveUsername, userStore } from '@/stores/user'
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useUser } from '@/features/auth/api/useUser'
import { useConfig } from '@/api/useConfig'
const onError = (e: Error) => console.error('ERROR', e) const onError = (e: Error) => console.error('ERROR', e)
@@ -117,9 +114,6 @@ export const Join = ({
}) => { }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' }) const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { data: configData } = useConfig()
const { isLoggedIn, user } = useUser()
const { const {
audioEnabled, audioEnabled,
videoEnabled, videoEnabled,
@@ -127,10 +121,9 @@ export const Join = ({
audioOutputDeviceId, audioOutputDeviceId,
videoDeviceId, videoDeviceId,
processorConfig, processorConfig,
username,
} = useSnapshot(userChoicesStore) } = useSnapshot(userChoicesStore)
const { username } = useSnapshot(userStore)
const initialUserChoices = useRef<LocalUserChoices | null>(null) const initialUserChoices = useRef<LocalUserChoices | null>(null)
if (initialUserChoices.current === null) { if (initialUserChoices.current === null) {
@@ -141,6 +134,7 @@ export const Join = ({
audioOutputDeviceId, audioOutputDeviceId,
videoDeviceId, videoDeviceId,
processorConfig, processorConfig,
username,
} }
} }
@@ -454,23 +448,20 @@ export const Join = ({
<H lvl={1} margin="sm" centered> <H lvl={1} margin="sm" centered>
{t('heading')} {t('heading')}
</H> </H>
{(!isLoggedIn || <Field
configData?.authenticated_users_can_edit_display_name) && ( type="text"
<Field onChange={saveUsername}
type="text" label={t('usernameLabel')}
onChange={saveUsername} id="input-name"
label={t('usernameLabel')} defaultValue={username}
id="input-name" validate={(value) => !value && t('errors.usernameEmpty')}
defaultValue={username || user?.full_name} wrapperProps={{
validate={(value) => !value && t('errors.usernameEmpty')} noMargin: true,
wrapperProps={{ fullWidth: true,
noMargin: true, }}
fullWidth: true, autoComplete="name"
}} maxLength={50}
autoComplete="name" />
maxLength={50}
/>
)}
</VStack> </VStack>
</Form> </Form>
) )
@@ -2,7 +2,6 @@ import type { Participant } from 'livekit-client'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { Avatar } from '@/components/Avatar' import { Avatar } from '@/components/Avatar'
import { useIsSpeaking } from '@livekit/components-react' import { useIsSpeaking } from '@livekit/components-react'
import { getParticipantBackgroundGradient } from '@/features/rooms/utils/getParticipantBackgroundGradient'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor' import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver' import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
@@ -11,6 +10,7 @@ const StyledParticipantPlaceHolder = styled('div', {
base: { base: {
width: '100%', width: '100%',
height: '100%', height: '100%',
backgroundColor: 'primaryDark.100',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@@ -26,10 +26,6 @@ export const ParticipantPlaceholder = ({
}: ParticipantPlaceholderProps) => { }: ParticipantPlaceholderProps) => {
const isSpeaking = useIsSpeaking(participant) const isSpeaking = useIsSpeaking(participant)
const participantColor = getParticipantColor(participant) const participantColor = getParticipantColor(participant)
const backgroundGradient = useMemo(
() => getParticipantBackgroundGradient(participantColor),
[participantColor]
)
const placeholderEl = useRef<HTMLDivElement>(null) const placeholderEl = useRef<HTMLDivElement>(null)
const { width, height } = useSize(placeholderEl) const { width, height } = useSize(placeholderEl)
@@ -43,13 +39,7 @@ export const ParticipantPlaceholder = ({
const initialSize = useMemo(() => Math.round(avatarSize * 0.3), [avatarSize]) const initialSize = useMemo(() => Math.round(avatarSize * 0.3), [avatarSize])
return ( return (
<StyledParticipantPlaceHolder <StyledParticipantPlaceHolder ref={placeholderEl}>
ref={placeholderEl}
style={{
backgroundColor: participantColor,
backgroundImage: backgroundGradient,
}}
>
<div <div
style={{ style={{
borderRadius: '50%', borderRadius: '50%',
@@ -52,7 +52,6 @@ export function TrackRefContextIfNeeded(
interface ParticipantTileExtendedProps extends ParticipantTileProps { interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean disableMetadata?: boolean
disableTileControls?: boolean
} }
export const ParticipantTile: ( export const ParticipantTile: (
@@ -67,7 +66,6 @@ export const ParticipantTile: (
onParticipantClick, onParticipantClick,
disableSpeakingIndicator, disableSpeakingIndicator,
disableMetadata, disableMetadata,
disableTileControls,
...htmlProps ...htmlProps
}: ParticipantTileExtendedProps, }: ParticipantTileExtendedProps,
ref ref
@@ -233,7 +231,7 @@ export const ParticipantTile: (
)} )}
</> </>
)} )}
{!disableMetadata && !disableTileControls && ( {!disableMetadata && (
<ParticipantTileFocus <ParticipantTileFocus
trackRef={trackReference} trackRef={trackReference}
hasKeyboardFocus={hasKeyboardFocus} hasKeyboardFocus={hasKeyboardFocus}
@@ -6,8 +6,8 @@ import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react' import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList' import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { PanelId, useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import React, { ReactNode, useCallback, useRef } from 'react' import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat' import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects' import { Effects } from './effects/Effects'
import { Admin } from './Admin' import { Admin } from './Admin'
@@ -15,7 +15,6 @@ import { Tools } from './Tools'
import { Info } from './Info' import { Info } from './Info'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar' import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
type StyledSidePanelProps = { type StyledSidePanelProps = {
title: string title: string
@@ -30,115 +29,103 @@ type StyledSidePanelProps = {
isReactionToolbarOpen?: boolean isReactionToolbarOpen?: boolean
} }
const StyledSidePanel = React.forwardRef<HTMLElement, StyledSidePanelProps>( const StyledSidePanel = ({
( title,
{ ariaLabel,
title, children,
ariaLabel, onClose,
children, isClosed,
onClose, isReactionToolbarOpen,
isClosed, closeButtonTooltip,
isReactionToolbarOpen, isSubmenu = false,
closeButtonTooltip, onBack,
isSubmenu = false, backButtonLabel,
onBack, }: StyledSidePanelProps) => (
backButtonLabel, <aside
}, className={css({
ref borderWidth: '1px',
) => ( borderStyle: 'solid',
<aside borderColor: 'box.border',
ref={ref} backgroundColor: 'box.bg',
tabIndex={-1} color: 'box.text',
className={css({ borderRadius: 8,
borderWidth: '1px', flex: 1,
borderStyle: 'solid', position: 'absolute',
borderColor: 'box.border', overflow: 'hidden',
backgroundColor: 'box.bg', display: 'flex',
color: 'box.text', flexDirection: 'column',
borderRadius: 8, margin: 'var(--sizes-room-side-panel-margin)',
flex: 1, marginLeft: 0,
position: 'absolute', marginBottom: 0,
overflow: 'hidden', padding: 0,
display: 'flex', gap: 0,
flexDirection: 'column', right: 0,
margin: 'var(--sizes-room-side-panel-margin)', top: 0,
marginLeft: 0, width: 'var(--sizes-room-side-panel)',
marginBottom: 0, transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
padding: 0, })}
gap: 0, style={{
right: 0, transform: isClosed
top: 0, ? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))'
width: 'var(--sizes-room-side-panel)', : 'none',
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms', bottom: isReactionToolbarOpen
'&:focus': { ? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))'
outline: 'none', : 'var(--sizes-room-control-bar)',
}, }}
})} aria-hidden={isClosed}
style={{ aria-label={ariaLabel}
transform: isClosed >
? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))' <HStack alignItems="center">
: 'none', {isSubmenu && (
bottom: isReactionToolbarOpen <Button
? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))' variant="secondaryText"
: 'var(--sizes-room-control-bar)', size="sm"
}} square
aria-hidden={isClosed} className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={ariaLabel} aria-label={backButtonLabel}
> onPress={onBack}
<HStack alignItems="center">
{isSubmenu && (
<Button
variant="secondaryText"
size="sm"
square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel}
onPress={onBack}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
</Button>
)}
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
> >
{title} <RiArrowLeftLine size={20} aria-hidden="true" />
</Heading> </Button>
</HStack> )}
<Div <Heading
position="absolute" slot="title"
top="5" level={1}
right="5" className={text({ variant: 'h2' })}
style={{ style={{
display: isClosed ? 'none' : undefined, paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}} }}
> >
<Button {title}
invisible </Heading>
variant="tertiaryText" </HStack>
size="xs" <Div
onPress={onClose} position="absolute"
aria-label={closeButtonTooltip} top="5"
tooltip={closeButtonTooltip} right="5"
> style={{
<RiCloseLine /> display: isClosed ? 'none' : undefined,
</Button> }}
</Div> >
{children} <Button
</aside> invisible
) variant="tertiaryText"
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
{children}
</aside>
) )
StyledSidePanel.displayName = 'StyledSidePanel'
type PanelProps = { type PanelProps = {
isOpen: boolean isOpen: boolean
children: React.ReactNode children: React.ReactNode
@@ -175,28 +162,8 @@ export const SidePanel = () => {
const { isOpen: isReactionToolbarOpen } = useReactionsToolbar() const { isOpen: isReactionToolbarOpen } = useReactionsToolbar()
const asideRef = useRef<HTMLElement>(null)
const focusAside = useCallback(() => {
requestAnimationFrame(() => {
asideRef.current?.focus({ preventScroll: true })
})
}, [])
const handlePanelOpened = useCallback(() => {
if (activePanelId === PanelId.CHAT) return
focusAside()
}, [activePanelId, focusAside])
useRestoreFocus(isSidePanelOpen, {
onOpened: handlePanelOpened,
preventScroll: true,
activeKey: activePanelId,
})
return ( return (
<StyledSidePanel <StyledSidePanel
ref={asideRef}
title={title} title={title}
ariaLabel={t('ariaLabel', { title })} ariaLabel={t('ariaLabel', { title })}
onClose={() => { onClose={() => {
@@ -95,13 +95,8 @@ const ToolButton = ({
export const Tools = () => { export const Tools = () => {
const { data } = useConfig() const { data } = useConfig()
const { const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } =
openTranscript, useSidePanel()
openScreenRecording,
activeSubPanelId,
isToolsOpen,
isSidePanelOpen,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
// Restore focus to the element that opened the Tools panel // Restore focus to the element that opened the Tools panel
@@ -118,7 +113,6 @@ export const Tools = () => {
}, },
restoreFocusRaf: true, restoreFocusRaf: true,
preventScroll: true, preventScroll: true,
shouldRestoreOnClose: () => !isSidePanelOpen,
}) })
const isTranscriptEnabled = useIsRecordingModeEnabled( const isTranscriptEnabled = useIsRecordingModeEnabled(
@@ -13,10 +13,8 @@ import {
} from './TimerWorker' } from './TimerWorker'
import { import {
BackgroundProcessorInterface, BackgroundProcessorInterface,
SELFIE_SEGMENTER_MODEL_PATH,
type ProcessorConfig, type ProcessorConfig,
type ProcessorType, type ProcessorType,
MEDIAPIPE_PATH_WASM,
} from '.' } from '.'
const PROCESSING_WIDTH = 256 const PROCESSING_WIDTH = 256
@@ -155,10 +153,13 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
} }
async initSegmenter() { async initSegmenter() {
const vision = await FilesetResolver.forVisionTasks(MEDIAPIPE_PATH_WASM) const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
)
this.imageSegmenter = await ImageSegmenter.createFromOptions(vision, { this.imageSegmenter = await ImageSegmenter.createFromOptions(vision, {
baseOptions: { baseOptions: {
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH, modelAssetPath:
'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter_landscape/float16/latest/selfie_segmenter_landscape.tflite',
delegate: 'CPU', // Use CPU for Firefox. delegate: 'CPU', // Use CPU for Firefox.
}, },
runningMode: 'VIDEO', runningMode: 'VIDEO',
@@ -11,11 +11,7 @@ import {
TIMEOUT_TICK, TIMEOUT_TICK,
timerWorkerScript, timerWorkerScript,
} from './TimerWorker' } from './TimerWorker'
import { import { ProcessorType } from '.'
FACE_LANDMARKS_MODEL_PATH,
ProcessorType,
MEDIAPIPE_PATH_WASM,
} from '.'
const PROCESSING_WIDTH = 256 * 3 const PROCESSING_WIDTH = 256 * 3
const PROCESSING_HEIGHT = 144 * 3 const PROCESSING_HEIGHT = 144 * 3
@@ -132,10 +128,13 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
} }
async initFaceLandmarker() { async initFaceLandmarker() {
const vision = await FilesetResolver.forVisionTasks(MEDIAPIPE_PATH_WASM) const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
)
this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, { this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: { baseOptions: {
modelAssetPath: FACE_LANDMARKS_MODEL_PATH, modelAssetPath:
'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task',
delegate: 'GPU', delegate: 'GPU',
}, },
runningMode: 'VIDEO', runningMode: 'VIDEO',
@@ -1,14 +1,13 @@
import type { ProcessorOptions, Track } from 'livekit-client' import type { ProcessorOptions, Track } from 'livekit-client'
import { import {
BackgroundBlur,
ProcessorWrapper, ProcessorWrapper,
BackgroundProcessor, VirtualBackground,
} from '@livekit/track-processors' } from '@livekit/track-processors'
import { import {
type ProcessorConfig, type ProcessorConfig,
BackgroundProcessorInterface, BackgroundProcessorInterface,
ProcessorType, ProcessorType,
MEDIAPIPE_PATH_WASM,
SELFIE_SEGMENTER_MODEL_PATH,
} from '.' } from '.'
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface { export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
@@ -21,24 +20,10 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
if (opts.type === 'virtual') { if (opts.type === 'virtual') {
this.processorType = ProcessorType.VIRTUAL this.processorType = ProcessorType.VIRTUAL
this.processor = BackgroundProcessor({ this.processor = VirtualBackground(opts.imagePath)
mode: 'virtual-background',
imagePath: opts.imagePath,
assetPaths: {
tasksVisionFileSet: MEDIAPIPE_PATH_WASM,
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH,
},
})
} else if (opts.type === 'blur') { } else if (opts.type === 'blur') {
this.processorType = ProcessorType.BLUR this.processorType = ProcessorType.BLUR
this.processor = BackgroundProcessor({ this.processor = BackgroundBlur(opts.blurRadius)
mode: 'background-blur',
blurRadius: opts.blurRadius,
assetPaths: {
tasksVisionFileSet: MEDIAPIPE_PATH_WASM,
modelAssetPath: SELFIE_SEGMENTER_MODEL_PATH,
},
})
} else { } else {
throw new Error( throw new Error(
'Must provide either imagePath for virtual background or blurRadius for blur' 'Must provide either imagePath for virtual background or blurRadius for blur'
@@ -4,14 +4,6 @@ import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor' import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
import { FaceLandmarksOptions } from './FaceLandmarksProcessor' import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
export const SELFIE_SEGMENTER_MODEL_PATH =
'/assets/mediapipe/models/selfie_segmenter_landscape.tflite'
export const FACE_LANDMARKS_MODEL_PATH =
'/assets/mediapipe/models/face_landmarker.task'
export const MEDIAPIPE_PATH_WASM = '/assets/mediapipe/wasm'
export enum ProcessorType { export enum ProcessorType {
BLUR = 'blur', BLUR = 'blur',
VIRTUAL = 'virtual', VIRTUAL = 'virtual',
@@ -37,7 +37,7 @@ export function Chat({ ...props }: ChatProps) {
const room = useRoomContext() const room = useRoomContext()
const { send, chatMessages, isSending } = useChat() const { send, chatMessages, isSending } = useChat()
const { isChatOpen, isSidePanelOpen } = useSidePanel() const { isChatOpen } = useSidePanel()
const chatSnap = useSnapshot(chatStore) const chatSnap = useSnapshot(chatStore)
// Keep track of the element that opened the chat so we can restore focus // Keep track of the element that opened the chat so we can restore focus
@@ -51,7 +51,6 @@ export function Chat({ ...props }: ChatProps) {
}) })
}, },
preventScroll: true, preventScroll: true,
shouldRestoreOnClose: () => !isSidePanelOpen,
}) })
// Use useParticipants hook to trigger a re-render when the participant list changes. // Use useParticipants hook to trigger a re-render when the participant list changes.
@@ -1,12 +0,0 @@
/**
* Builds a radial gradient from the participant's avatar color:
* brighter in the center (where the avatar sits), darker at the edges.
* Mixed in oklch so the color stays vivid as it darkens instead of greying out.
*/
export const getParticipantBackgroundGradient = (color: string): string =>
`radial-gradient(circle at 50% 45%,
color-mix(in oklch, ${color} 82%, white) 0%,
color-mix(in oklch, ${color} 90%, white) 8%,
${color} 35%,
color-mix(in oklch, ${color} 85%, black) 65%,
color-mix(in oklch, ${color} 65%, black) 100%)`
@@ -8,9 +8,8 @@ import { HStack } from '@/styled-system/jsx'
import { useState } from 'react' import { useState } from 'react'
import { LoginButton } from '@/components/LoginButton' import { LoginButton } from '@/components/LoginButton'
import { useRenameParticipant } from '@/features/rooms/api/renameParticipant' import { useRenameParticipant } from '@/features/rooms/api/renameParticipant'
import { saveUsername } from '@/stores/user' import { saveUsername } from '@/stores/userChoices'
import { logout } from '@/features/auth/utils/logout' import { logout } from '@/features/auth/utils/logout'
import { useConfig } from '@/api/useConfig'
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> & export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'> Pick<TabPanelProps, 'id'>
@@ -18,7 +17,6 @@ export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
const { t } = useTranslation('settings') const { t } = useTranslation('settings')
const room = useRoomContext() const room = useRoomContext()
const { data } = useConfig()
const { user, isLoggedIn } = useUser() const { user, isLoggedIn } = useUser()
const { renameParticipant } = useRenameParticipant() const { renameParticipant } = useRenameParticipant()
@@ -47,17 +45,15 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
return ( return (
<TabPanel padding={'md'} flex id={id}> <TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('account.heading')}</H> <H lvl={2}>{t('account.heading')}</H>
{(!isLoggedIn || data?.authenticated_users_can_edit_display_name) && ( <Field
<Field type="text"
type="text" label={t('account.nameLabel')}
label={t('account.nameLabel')} value={name}
value={name} onChange={setName}
onChange={setName} validate={(value) => {
validate={(value) => { return !value ? <p>{t('account.nameError')}</p> : null
return !value ? <p>{t('account.nameError')}</p> : null }}
}} />
/>
)}
<H lvl={2}>{t('account.authentication')}</H> <H lvl={2}>{t('account.authentication')}</H>
{isLoggedIn ? ( {isLoggedIn ? (
<> <>
+1 -24
View File
@@ -6,10 +6,6 @@ export type RestoreFocusOptions = {
onClosed?: () => void onClosed?: () => void
restoreFocusRaf?: boolean restoreFocusRaf?: boolean
preventScroll?: boolean preventScroll?: boolean
/** When the panel stays open but its content changes, update the restore target. */
activeKey?: string | null
/** Return false to skip restoring focus on close (e.g. when switching to another panel). */
shouldRestoreOnClose?: () => boolean
} }
/** /**
@@ -26,12 +22,9 @@ export function useRestoreFocus(
onClosed, onClosed,
restoreFocusRaf = false, restoreFocusRaf = false,
preventScroll = true, preventScroll = true,
activeKey,
shouldRestoreOnClose,
} = options } = options
const prevIsOpenRef = useRef(false) const prevIsOpenRef = useRef(false)
const prevActiveKeyRef = useRef(activeKey)
const triggerRef = useRef<HTMLElement | null>(null) const triggerRef = useRef<HTMLElement | null>(null)
useEffect(() => { useEffect(() => {
@@ -44,41 +37,25 @@ export function useRestoreFocus(
onOpened?.() onOpened?.()
} }
// Panel switched while staying open
if (wasOpen && isOpen && activeKey !== prevActiveKeyRef.current) {
const activeEl = document.activeElement as HTMLElement | null
triggerRef.current = resolveTrigger ? resolveTrigger(activeEl) : activeEl
onOpened?.()
}
// Just closed // Just closed
if (wasOpen && !isOpen) { if (wasOpen && !isOpen) {
const trigger = triggerRef.current const trigger = triggerRef.current
const shouldRestore = if (trigger && document.contains(trigger)) {
(shouldRestoreOnClose?.() ?? true) &&
trigger &&
document.contains(trigger)
if (shouldRestore) {
const focus = () => trigger.focus({ preventScroll }) const focus = () => trigger.focus({ preventScroll })
if (restoreFocusRaf) requestAnimationFrame(focus) if (restoreFocusRaf) requestAnimationFrame(focus)
else focus() else focus()
} }
triggerRef.current = null triggerRef.current = null
onClosed?.() onClosed?.()
} }
prevIsOpenRef.current = isOpen prevIsOpenRef.current = isOpen
prevActiveKeyRef.current = activeKey
}, [ }, [
isOpen, isOpen,
activeKey,
onClosed, onClosed,
onOpened, onOpened,
preventScroll, preventScroll,
resolveTrigger, resolveTrigger,
restoreFocusRaf, restoreFocusRaf,
shouldRestoreOnClose,
]) ])
} }
-37
View File
@@ -1,37 +0,0 @@
import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys'
type State = {
username: string
}
const DEFAULT_STATE = {
username: '',
}
function getUserState(): State {
try {
const stored = localStorage.getItem(STORAGE_KEYS.USER)
if (!stored) return DEFAULT_STATE
const parsed = JSON.parse(stored)
return {
...parsed,
}
} catch (error: unknown) {
console.error(
'[UserPreferencesStore] Failed to parse stored settings:',
error
)
return DEFAULT_STATE
}
}
export const userStore = proxy<State>(getUserState())
subscribe(userStore, () => {
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userStore))
})
export const saveUsername = (username: string) => {
userStore.username = username
}
+6 -8
View File
@@ -12,7 +12,7 @@ import { VideoQuality } from 'livekit-client'
export type VideoResolution = 'h720' | 'h360' | 'h180' export type VideoResolution = 'h720' | 'h360' | 'h180'
export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & { export type LocalUserChoices = LocalUserChoicesLK & {
processorConfig?: ProcessorConfig processorConfig?: ProcessorConfig
noiseReductionEnabled?: boolean noiseReductionEnabled?: boolean
audioOutputDeviceId?: string audioOutputDeviceId?: string
@@ -32,13 +32,7 @@ function getUserChoicesState(): LocalUserChoices {
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState()) export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
subscribe(userChoicesStore, () => { subscribe(userChoicesStore, () => {
// TEMPORARY: cast needed because our store omits `username`, which we no saveUserChoices(userChoicesStore, false)
// longer persis in this store, while LiveKit's `saveUserChoices` still expects the full
// `LocalUserChoices` shape. `username` ends up `undefined` in the saved
// object, which `saveUserChoices` tolerates at runtime.
// We are migrating away from LiveKit's persistence logic to our own store
// handling for more control — this cast can be removed once that lands.
saveUserChoices(userChoicesStore as LocalUserChoicesLK, false)
}) })
// we run some logic on store loading to check if the processor config is still valid // we run some logic on store loading to check if the processor config is still valid
@@ -96,6 +90,10 @@ export const saveVideoSubscribeQuality = (quality: VideoQuality) => {
userChoicesStore.videoSubscribeQuality = quality userChoicesStore.videoSubscribeQuality = quality
} }
export const saveUsername = (username: string) => {
userChoicesStore.username = username
}
export const saveNoiseReductionEnabled = (enabled: boolean) => { export const saveNoiseReductionEnabled = (enabled: boolean) => {
userChoicesStore.noiseReductionEnabled = enabled userChoicesStore.noiseReductionEnabled = enabled
} }
-1
View File
@@ -4,6 +4,5 @@
export const STORAGE_KEYS = { export const STORAGE_KEYS = {
NOTIFICATIONS: 'app_notification_settings', NOTIFICATIONS: 'app_notification_settings',
USER_PREFERENCES: 'app_user_preferences', USER_PREFERENCES: 'app_user_preferences',
USER: 'app_user',
ACCESSIBILITY: 'app_accessibility_settings', ACCESSIBILITY: 'app_accessibility_settings',
} as const } as const
+7 -22
View File
@@ -2,7 +2,6 @@ import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import svgr from 'vite-plugin-svgr' import svgr from 'vite-plugin-svgr'
import { viteStaticCopy } from 'vite-plugin-static-copy'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
@@ -12,32 +11,18 @@ export default defineConfig(({ mode }) => {
react(), react(),
svgr({ svgr({
svgrOptions: { svgrOptions: {
replaceAttrValues: { replaceAttrValues: { '#000': 'currentColor', '#000000': 'currentColor', '#1f1f1f': 'currentColor' },
'#000': 'currentColor',
'#000000': 'currentColor',
'#1f1f1f': 'currentColor',
},
}, },
}), }),
viteStaticCopy({ env.VITE_ANALYZE === 'true' && visualizer({
targets: [ open: true,
{ filename: 'rollup-plugin-visualizer/stats.html',
src: 'node_modules/@mediapipe/tasks-vision/wasm/*', gzipSize: true,
dest: 'assets/mediapipe/wasm', brotliSize: true,
rename: { stripBase: 4 },
},
],
}), }),
env.VITE_ANALYZE === 'true' &&
visualizer({
open: true,
filename: 'rollup-plugin-visualizer/stats.html',
gzipSize: true,
brotliSize: true,
}),
], ],
resolve: { resolve: {
tsconfigPaths: true, tsconfigPaths: true
}, },
build: { build: {
sourcemap: env.VITE_BUILD_SOURCEMAP === 'true', sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
-2
View File
@@ -191,8 +191,6 @@ backend:
SUMMARY_SERVICE_API_TOKEN: password SUMMARY_SERVICE_API_TOKEN: password
SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
OIDC_USERINFO_FULLNAME_FIELDS: first_name, last_name
OIDC_USERINFO_SHORTNAME_FIELD: first_name
migrate: migrate:
command: command:
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2 apiVersion: v2
type: application type: application
name: meet name: meet
version: 0.0.27 version: 0.0.26
+1 -170
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp"> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>{{ .id }}</Id> <Id>{{ .id }}</Id>
<Version>1.0.0.0</Version> <Version>0.0.2.0</Version>
<ProviderName>{{ .appName }}</ProviderName> <ProviderName>{{ .appName }}</ProviderName>
<DefaultLocale>fr-FR</DefaultLocale> <DefaultLocale>fr-FR</DefaultLocale>
<DisplayName DefaultValue="{{ .appName }}"/> <DisplayName DefaultValue="{{ .appName }}"/>
@@ -205,174 +205,5 @@
</bt:String> </bt:String>
</bt:LongStrings> </bt:LongStrings>
</Resources> </Resources>
<!-- ─── V1.1 override: MUST be nested inside the V1.0 block, after Resources ─── -->
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
<Requirements>
<bt:Sets DefaultMinVersion="1.8">
<bt:Set Name="Mailbox"/>
</bt:Sets>
</Requirements>
<Hosts>
<Host xsi:type="MailHost">
<DesktopFormFactor>
<SupportsSharedFolders>true</SupportsSharedFolders>
<FunctionFile resid="Commands.Url"/>
<!-- ─── Mail: Read ─────────────────────────────────────────── -->
<ExtensionPoint xsi:type="MessageReadCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgReadGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgReadOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- ─── Mail: Compose ─────────────────────────────────────── -->
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgComposeGenerateLinkButton">
<Label resid="GenerateLink.Label"/>
<Supertip>
<Title resid="GenerateLink.Label"/>
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromMail</FunctionName>
</Action>
</Control>
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Settings.16x16"/>
<bt:Image size="32" resid="Settings.32x32"/>
<bt:Image size="80" resid="Settings.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- ─── Calendar: Compose (New/Edit appointment) ──────────── -->
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
<OfficeTab id="TabDefault">
<Group id="apptComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="apptGenerateLinkButton">
<Label resid="GenerateLink.Label"/>
<Supertip>
<Title resid="GenerateLink.Label"/>
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
</Action>
</Control>
<Control xsi:type="Button" id="apptOpenSettingsButton">
<Label resid="OpenSettings.Label"/>
<Supertip>
<Title resid="OpenSettings.Label"/>
<Description resid="OpenSettings.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Settings.16x16"/>
<bt:Image size="32" resid="Settings.32x32"/>
<bt:Image size="80" resid="Settings.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Settings.16x16" DefaultValue="{{ .baseUrl }}/assets/settings-16.png"/>
<bt:Image id="Settings.32x32" DefaultValue="{{ .baseUrl }}/assets/settings-32.png"/>
<bt:Image id="Settings.80x80" DefaultValue="{{ .baseUrl }}/assets/settings-80.png"/>
<bt:Image id="Add.16x16" DefaultValue="{{ .baseUrl }}/assets/add-16.png"/>
<bt:Image id="Add.32x32" DefaultValue="{{ .baseUrl }}/assets/add-32.png"/>
<bt:Image id="Add.80x80" DefaultValue="{{ .baseUrl }}/assets/add-80.png"/>
<bt:Image id="Icon.16x16" DefaultValue="{{ .baseUrl }}/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="{{ .baseUrl }}/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="{{ .baseUrl }}/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Commands.Url" DefaultValue="{{ .baseUrl }}/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="{{ .baseUrl }}/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<!-- Default (French) -->
<bt:String id="GroupLabel" DefaultValue="{{ .appName }}"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien {{ .appName }}">
<bt:Override Locale="en-US" Value="Add a {{ .appName }} link"/>
<bt:Override Locale="de-DE" Value="{{ .appName }}-Link hinzufügen"/>
</bt:String>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
<bt:Override Locale="en-US" Value="Open settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
</bt:String>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
<bt:Override Locale="en-US" Value="Settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen"/>
</bt:String>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion {{ .appName }} et l'insère dans l'événement.">
<bt:Override Locale="de-DE" Value="Generiert einen {{ .appName }}-Besprechungslink und fügt ihn in den Termin ein."/>
<bt:Override Locale="en-US" Value="Generates a {{ .appName }} meeting link and inserts it into the item."/>
</bt:String>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
</bt:String>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
</bt:String>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</VersionOverrides> </VersionOverrides>
</OfficeApp> </OfficeApp>
+3 -18
View File
@@ -18,12 +18,7 @@ from requests import exceptions
from summary.core.analytics import MetadataManager, get_analytics from summary.core.analytics import MetadataManager, get_analytics
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.docs_service import create_document_in_lasuite_docs from summary.core.docs_service import create_document_in_lasuite_docs
from summary.core.file_service import ( from summary.core.file_service import FileService, FileServiceException, TranscribeError
CorruptedAudioFile,
FileService,
FileServiceException,
TranscribeError,
)
from summary.core.llm_service import LLMException, LLMObservability, LLMService from summary.core.llm_service import LLMException, LLMObservability, LLMService
from summary.core.locales import get_locale from summary.core.locales import get_locale
from summary.core.models import ( from summary.core.models import (
@@ -152,20 +147,10 @@ def transcribe_audio(
# Mimic OpenAI's timeout settings # Mimic OpenAI's timeout settings
timeout=(60, 10 * 60), timeout=(60, 10 * 60),
) )
if res.status_code == 400:
logger.info(
"WhisperX transcription failed, "
"likely due to a corrupted audio file: %s",
res.text,
)
raise CorruptedAudioFile("WhisperX coudln't decode the audio file.")
try: try:
res.raise_for_status() res.raise_for_status()
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError as e:
logger.exception("WhisperX transcription failed") raise RuntimeError("WhisperX transcription failed, %s", res.text) from e
# We reraise the error so that it can be retried by celery
raise
transcription_json: dict[str, Any] = res.json() transcription_json: dict[str, Any] = res.json()
# We remove the "usage" key from the transcription_json dictionary # We remove the "usage" key from the transcription_json dictionary
-6
View File
@@ -41,12 +41,6 @@ class NoAudioInFileError(TranscribeError):
error_code = "no_audio_in_file" error_code = "no_audio_in_file"
class CorruptedAudioFile(TranscribeError):
"""Raised when a media file does not contain any audio."""
error_code = "corrupted_audio_file"
def _get_duration_from_packets(local_path: Path) -> float: def _get_duration_from_packets(local_path: Path) -> float:
"""Estimate duration from audio packet timestamps.""" """Estimate duration from audio packet timestamps."""
# Run ffprobe to inspect the first audio stream in the file. # Run ffprobe to inspect the first audio stream in the file.
+1 -7
View File
@@ -96,13 +96,7 @@ class TranscribeWebhookFailurePayload(BaseWebhook):
# we authorized any other string than the one in the literal # we authorized any other string than the one in the literal
# to avoid causing a breaking change on the client side # to avoid causing a breaking change on the client side
error_code: ( error_code: (
Literal[ Literal["unknown_error", "no_audio_in_file", "media_duration_too_long"] | str
"unknown_error",
"no_audio_in_file",
"media_duration_too_long",
"corrupted_audio_file",
]
| str
) = Field(title="Error code", description="The error code.") ) = Field(title="Error code", description="The error code.")