Commit Graph
100 Commits
Author SHA1 Message Date
lebaudantoineandaleb_the_flash 751d029ac9 🔧(frontend) sync persisted device ids with the actual selected devices
Now that the exact deviceId constraint has been dropped, the browser
can pick a different device than the one persisted in localStorage
(for example when the persisted device is no longer available).

Sync the persisted ids in localStorage with the device id that was
actually selected on the started track, so the local cache stays
consistent with what the app is really using.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash aaa51a4457 ️(frontend) revert old permission-toggle hotfix
Revert the old hotfix that allowed users to toggle their microphone
or camera while permissions were not granted, which then triggered
a `getUserMedia` call to prompt for them.

Now that the permission store is properly kept in sync with the
browser, this workaround is no longer needed as-is. The intended
behavior will be reimplemented cleanly in a later commit.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash c8a3ef6f61 🐛(frontend) fix permission store regression
`derive-valtio` was broken by a recent update, which cascaded into
various regressions in the permission store.

Take the opportunity to also refactor how permissions are handled.
The store is now a pure cache with a single writer: every signal
re-reads the browser via `syncPermissions()`, and the browser stays
the only source of truth.

Re-sync triggers, all event-driven (no polling):

* `devicechange`: granting permission reveals device labels/ids, so
  it fires on grant in every browser, including Safari. This
  replaces the previous 500ms Safari polling. Denials are still
  caught by the concurrent `getUserMedia` rejection through
  `notePermissionDeniedFromGum`.
* Window focus: covers the return from the browser or system
  permission UI.
* Permissions API `change` events, where the query is supported.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 5d50671b3c 🔥(frontend) remove buggy device-id resolution code
Remove the current device-id resolution code that was buggy and
failed to resolve the device id correctly.

A replacement will be introduced in upcoming commits.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 8615bf879c 📈(frontend) capture media diagnostics on media errors
Attach a media diagnostics snapshot to the room event handler for
media exceptions. The snapshot captures the state of the user's
setup at the moment of the error (available devices, permission
state, active tracks, etc.), so support has enough context to
troubleshoot user issues without asking them to reproduce.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 186d16c46f 🐛(frontend) drop exact deviceId constraint on dynamic track creation
Dynamic track creation used an exact deviceId constraint based on
the device id persisted in localStorage. If that device was no
longer available on reconnect, the browser raised a DOMException
instead of falling back to another device.

Drop the exact constraint so the browser can pick any available
device when the persisted one is gone.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash fb3ee56702 ♻️(frontend) encapsulate PostHog capture calls in the telemetry module
Move the remaining direct `posthog.capture` calls behind the
telemetry module, so PostHog is only referenced from a single place.

Call sites now use the telemetry API instead of touching PostHog
directly, making it easier to swap the backend later without
changing every call site.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 48c0cb320e ♻️(frontend) encapsulate error tracking behind a telemetry module
Introduce a telemetry module that exposes a `reportError` helper.
Under the hood it forwards errors to PostHog, but the module is the
only place that knows about PostHog.

Replace `console.error` calls used for error reporting with
`reportError`, so the codebase now goes through a single, consistent
API for telemetry.

This normalizes how errors are reported and makes it straightforward
to swap PostHog for another backend later on, without touching every
call site.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash d810c9e0de 🐛(frontend) drop resize listener in useIsMobileBrowser
`isMobileBrowser()` only reads `navigator.userAgent`, which does
not change during the lifetime of the document, so the previous
`resize` listener never had anything meaningful to update.

It did, however, dispatch `setIsMobile` on components rendered into
a Document Picture-in-Picture window (e.g. the reactions toolbar).
When the PiP window had already been closed, Firefox threw
"can't access dead object".

Compute the value once and skip the listener entirely.

Fix 019cb315-d827-73f2-b1cc-74e4dd71e982
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 134d9a188f 🐛(frontend) gate blur on WebGL2 transformer support
`ProcessorWrapper.isSupported` reports pipeline support but not
whether the WebGL2 transformer is available. On browsers where it
is not (e.g. Chrome/Edge on Windows with WebGL2 disabled by a GPU
blocklist), toggling blur throws at runtime.

Update `supportsBackgroundProcessors()` to check both, so the UI
only exposes blur when it can actually run.

fix 019f8e3b-f035-73e2-9d6a-d0dd2d0a1163
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash ea7188059d 🐛(frontend) guard getRouteUrl('room', slug) against missing slug
InviteDialog.tsx and Info.tsx were the last call sites calling
getRouteUrl('room', slug) without a slug guard, unlike every other
caller (e.g. useCopyRoomToClipboard).

Compute roomUrl only when the slug exists (undefined in
InviteDialog, '' in Info to keep its unguarded .replace safe).
Guarding at the call site preserves the "no room data yet" state
instead of returning a bogus "/" URL from room.to.

Fix 019fd616-f158-7771-8cff-bac3090b8449
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash b8958e6e87 🐛(frontend) unmount PiP portal synchronously on pagehide
When the PiP window closes, the browser destroys its document right
after `pagehide`. If the portal unmount is left to React's async
scheduling, it commits against a dead document and `removeChild`
throws "NotFoundError", crashing the app.

Subscribe `PictureInPicturePortal` to the Valtio store with
`sync: true`, and use `flushSync` in `usePictureInPicture` on
teardown so React unmounts the portal while the PiP document is
still alive.

Fix 019f42cf-86a9-7ad2-8e64-81b004ddc5de
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash 23bb3c39d0 🐛(frontend) normalize thrown values into proper Error instances
LiveKit can surface raw DOM events (for example WebSocket "error"
events, whose only enumerable key is `isTrusted`) instead of Error
instances.

When such a value ends up being captured, our error reporting logs
it as "Event: Event captured as exception with keys: isTrusted",
which is unhelpful and hides the real cause.

Add a small helper that normalizes any unknown thrown or emitted
value into a proper Error, preserving the original payload as
context.

Fixes 01997b9a-db63-7fc2-8fe4-f21dd7fd608d.
2026-08-12 14:52:09 +02:00
lebaudantoineandaleb_the_flash e0ff28ed48 🔖(patch) release 1.25.22 2026-08-06 13:29:17 +02:00
lebaudantoineandaleb_the_flash 61e8b597dc 🐛(frontend) harmonize cache configuration for MediaPipe assets
The wasm and js files shipped by MediaPipe were served with
different cache policies, which could leave the two out of sync on
the client (fresh js with stale wasm, or vice versa).

Align the cache configuration across the MediaPipe assets so they
are always cached and invalidated together.
2026-08-06 13:16:37 +02:00
lebaudantoineandaleb_the_flash f3626a2dc6 🐛(frontend) serve MediaPipe assets under a versioned path
The MediaPipe assets were served under /assets, where the cache
behavior differs between wasm and js files. As a result, clients
could end up with a fresh js loader paired with a stale wasm binary
(or vice versa), leaving MediaPipe out of sync.

Copy the assets under a versioned route so the URL changes whenever
the dependency version bumps. Clients then reload both the js and
the wasm together, keeping them in sync.
2026-08-06 13:16:37 +02:00
lebaudantoineandaleb_the_flash 41e937c1f1 🔖(patch)) release 1.25.1 2026-08-06 11:31:08 +02:00
lebaudantoine d988c72208 🚑️(frontend) fix background crash from MediaPipe WASM version mismatch 2026-08-06 11:27:26 +02:00
lebaudantoineandaleb_the_flash b96d591db3 🔖(minor) bump release to 1.25.0 2026-08-05 18:12:42 +02:00
lebaudantoineandaleb_the_flash bffc51ac4c 🐛(frontend) add trailing slash on the fetch-room URL
The fetch-room URL was missing its trailing slash, which caused the
backend to issue a 301 redirect. Query parameters were being dropped
in the process, leading to incorrect requests.

Append the trailing slash so the request hits the correct endpoint
directly, without going through a redirect.
2026-08-05 18:04:52 +02:00
lebaudantoineandaleb_the_flash 22a1713c60 🐛(frontend) fix concurrent PATCH races on room settings
Rapid toggles could persist a stale configuration: each PATCH
replaces the full room config, and every call site built it from a
render-time snapshot. A toggle issued before the previous one
resolved therefore overwrote the newer value with an older one.

Handle the cache centrally in usePatchRoom so the next toggle always
reads an up-to-date configuration.
2026-08-05 17:23:48 +02:00
lebaudantoineandaleb_the_flash c23f449520 🐛(frontend) stop passing username as a query param when undefined
Skip adding the username query parameter when its value is
undefined, so the request URL no longer ends up with an
`?username=undefined` (or similar) that the backend has to handle.
2026-08-05 17:23:48 +02:00
lebaudantoineandaleb_the_flash 0536896373 (sdk) add a room configuration popup from CreateMeetingButton
Introduce a room configuration popup opened from the SDK's
CreateMeetingButton, laid out like the Google Meet "call options"
dialog: logo header, grey section bands, and a footer bar with the
close action.

Like CreatePopup, it runs in a dedicated popup window so it can
access session cookies, which would be blocked in an iframe. If the
user is not authenticated, they are redirected to login and come
back to this popup afterwards.

Permissions are enforced server-side. The room is fetched with the
user's session, and settings are only shown when the room is
administrable by this user. Since #1482 removed the
is_administrable flag from the room serializer (roles now live in
the LiveKit participant attributes, only available in-meeting),
administrability is detected here through the presence of the
`accesses` field, which the backend only serializes for
administrators and owners. The PATCH endpoint enforces the same
permissions server-side regardless.

The settings mirror the in-room Admin panel. Unlike the Admin panel,
there is no LiveKit connection here, so changes are only persisted
in the room configuration (and applied when a session starts):
participants of an ongoing session are not live-synced or notified.
2026-08-05 17:23:48 +02:00
lebaudantoineandaleb_the_flash f49c61d9bf (sdk) allow passing a background color to the calendar iframe
Let integrators pass a custom background color to the iframe used by
the calendar SDK, so it can match the surrounding product's theme.
2026-08-05 17:23:48 +02:00
lebaudantoineandaleb_the_flash b593516802 🔒️(backend) derive connection-test room max age from token TTL
Refactor CONNECTION_TEST_ROOM_MAX_AGE_SECONDS so it is no longer an
independent setting but a quantity derived from (or added on top of)
the token TTL.

This prevents a misconfiguration where the token would outlive the
delete-room callback. In that case, an attacker holding a valid
token could recreate the room after the callback fired and escape
the intended cleanup.
2026-08-05 15:33:13 +02:00
lebaudantoineandaleb_the_flash 58205f81d4 🐛(frontend) fix icon centering in the Switch primitive
Icons inside the Switch primitive were not properly centered.

Use relative sizes for the icons and switch to a grid-based
placement strategy so they stay centered regardless of the switch
size.
2026-08-05 13:47:50 +02:00
lebaudantoineandaleb_the_flash b7892431be 💄(frontend) show pointer cursor on interactive switches
Set the cursor to a pointer on Switch components when they are
actually interactive, so it is visually clear that they can be
toggled.
2026-08-05 13:47:50 +02:00
lebaudantoineandaleb_the_flash 00a2bd9558 🐛(backend) serialize lazy title in summary payload
_generate_title returned a lazy gettext_lazy proxy in the
recording_datetime is None branch, which json.dumps cannot
serialize.

This crashed requests.post(json=payload) with "Object of type
__proxy__ is not JSON serializable" whenever the LiveKit egress
lookup failed (started_at=None).

Force evaluation with a non-lazy method.

Add a regression test asserting the v2 payload is a real str and
is JSON-serializable when timestamps are unavailable.
The existing without_metadata test missed this: mocked
requests.post never serialized, and a lazy proxy compares equal
to its string.
2026-08-05 12:48:45 +02:00
lebaudantoineandaleb_the_flash bc003f928e ⚗️(frontend) add candidate pair diagnostic to WebRTC checks
Add a custom diagnostic step that reports which ICE candidate pair
was selected on the WebRTC connection, as well as all working pairs
observed during the check.

Experimental and vibe-coded for now; the output is meant to help
debugging and will likely be revisited.
2026-08-05 12:16:40 +02:00
lebaudantoineandaleb_the_flash 6c69c3d6e3 🎨(frontend) apply lint fixes
Run the frontend linter/formatter and commit the resulting fixes to
keep the codebase clean and consistent.
2026-08-04 20:28:44 +02:00
lebaudantoineandaleb_the_flash 15c9ee225a ️(frontend) revert GridLayout re-render optimization too
Out of precaution, also revert the previous GridLayout re-render
optimization to avoid any layout regression alongside the
CarouselLayout revert.

The useSize-based re-render optimization will be reintroduced in a
dedicated small PR and release. That will also be a good occasion to
polish the layout code along the way.
2026-08-04 20:01:41 +02:00
lebaudantoineandaleb_the_flash d86768bb59 ️(frontend) revert CarouselLayout re-render optimization
The previous optimization of the CarouselLayout was broken: the
approach did not hold in practice, and strict-mode rendering was
hiding the issue during development.

Revert the change for now and revisit the optimization later with a
sounder approach.
2026-08-04 20:01:41 +02:00
lebaudantoine 7bd5ab7a13 (frontend) expose media state to external gateways
Add a hidden div in the DOM that reflects the current state of the
microphone and camera, so that external SIP media gateways (e.g. the
Renater one) can observe it and keep an accurate view of the media
state.

Also emit a custom event from the page whenever the microphone or
camera state changes, so external consumers can subscribe to updates
instead of polling the DOM.
2026-08-04 19:04:16 +02:00
lebaudantoine ac8eae7295 (backend) apply user preferences on unconfigured room creation
Update the API so that, when a user creates a new meeting without
passing an explicit configuration, the user's persisted preferences
are applied as defaults.

This allows a user to, for example, enable the waiting room by
default on every meeting they create.
2026-08-04 19:04:16 +02:00
lebaudantoine 15b1ab7e0a (frontend) let users set default configuration for generated links
Extend the existing out-of-room settings so users can configure a
default room configuration that is applied to every link they
generate from the app.
2026-08-04 19:04:16 +02:00
lebaudantoine 2e509eff28 (backend) persist user preferences for room defaults on the User model
Add attributes on the User model to persist per-user preferences for
the default link access level and the default room configuration.

The frontend will let users update these preferences and then reuse
them when generating a link through the webapp.

Persisting them on the backend (rather than in application memory
only) ensures the preferences survive across sessions and devices.
2026-08-04 19:04:16 +02:00
lebaudantoine ca56ae87c2 (backend) expose the default room access level in settings
Expose the default access level for rooms in the backend settings
response, so the frontend can initialize the global room preferences
UI with the current default value.
2026-08-04 19:04:15 +02:00
lebaudantoine 2c5a766d02 🔥(ci) remove unused Anthropic security step
Drop the Anthropic security step from the project CI, as it is no
longer used and only added noise to the pipeline.
2026-08-04 19:04:15 +02:00
lebaudantoine 9e500a59ea 🧑‍💻(backend) add commented the roomkit env variables
Useful for an easier devex when working on the feature.
2026-08-04 19:04:15 +02:00
lebaudantoine 8a0d4b1ad6 ♻️(backend) refactor tests to rely on decorators
Slightly refactor the existing tests to use decorators for common
setup and configuration.
2026-08-04 19:04:15 +02:00
lebaudantoine d0726ba631 🐛(backend) ensure SIP dispatch rule instead of creating it
The roomkit can now create a SIP dispatch rule before the LiveKit
webhook that used to trigger this creation is fired. In practice,
when the roomkit connects to the room, it also triggers the
webhook, leading to a duplicated dispatch rule.

Switch from "create dispatch rule" to "ensure dispatch rule exists"
semantics, so subsequent calls are idempotent and no duplicate rule
is created.
2026-08-04 19:04:14 +02:00
lebaudantoine e65036fe90 🚚(backend) rename TelephonyService to SIPManagement
Rename the telephony service to a more descriptive name,
SIPManagementService, which clearly states what the service is used
for.

It is no longer used only by the telephony feature; the roomkit
feature also relies on it now.
2026-08-04 19:04:14 +02:00
lebaudantoine 8b198726e9 (backend) add roomkit viewset to start a room without WebRTC join
Introduce a new viewset that lets the roomkit start a room even when
no WebRTC participant has joined yet.

This is a first entry point that will be extended over time with
more actions a roomkit needs to be able to trigger.

Known limitations:

* The responsibility around SIP rules is currently split between
  the telephony feature and the roomkit one. This may need a
  refactor later on to consolidate ownership in a single place.
* The default throttle might be too low for production usage and
  will likely need to be revisited.
2026-08-04 19:04:14 +02:00
lebaudantoine fc4774199b 🔧(devx) stop declaring LiveKit as an app-dev dependency
LiveKit was declared as an app-dev dependency, which caused it
(along with its egress) to be started whenever we ran unrelated
commands such as tests, migrate or makemigrations.

Drop that dependency and start LiveKit explicitly only when it is
actually needed, i.e. when calling run-backend.
2026-08-04 19:04:14 +02:00
lebaudantoine 9a3e54e6ec 🐛(backend) disable recording events in the default env file
The tests were failing when the Django settings did not disable
recording events, which was the case by default.

We do not rely on these events anymore by default, so set the
corresponding environment variable to false in the env file to make
the tests pass out of the box.
2026-08-04 19:04:14 +02:00
lebaudantoine be98c5210e 💄(frontend) adjust centering of Avatar initials
Fine-tune the vertical alignment of the initials in the Avatar so
they sit properly centered inside the circle.
2026-08-04 19:04:13 +02:00
lebaudantoine e400d05a7c 🚸(frontend) show two initials in the Avatar when possible
Display two initials in the Avatar whenever the participant's name
allows it, instead of a single letter.

A single initial makes it too hard to distinguish participants when
their cameras are off, especially in larger
2026-08-04 19:04:13 +02:00
lebaudantoine 9af42568bf 💄(frontend) improve participant name rendering in the list
Rework how the participant name is displayed in the participant
list to show as much of the name as possible before truncating.

When the name has to be truncated, add a tooltip so users can hover
to see the full name.

Requested by users.
2026-08-04 19:04:13 +02:00
lebaudantoine 7eba983c15 (frontend) introduce an "unauthenticated" participant badge
Add a visual badge on participants who are not authenticated, so it
is immediately clear who could be an anonymous participant. This is
a small but explicit security signal in the participant list.

Beyond that, the badge also plays a functional role: since only
authenticated participants can be promoted or demoted, the badge
helps users see at a glance who is eligible for a role change.
2026-08-04 19:04:12 +02:00
lebaudantoine b7210e62bb (frontend) notify user when their meeting role changes
Show a notification to the user whenever their role in the meeting
changes, so they immediately see when they have been promoted or
demoted.
2026-08-04 19:04:12 +02:00
lebaudantoine 653326347b 🐛(frontend) fall back to user.full_name on request-entry
Since the username refactoring, the username in the store could be
undefined when the join input was pre-filled from user.full_name,
because no keystroke was needed to populate the store.

This led to a 400 error on the request-entry endpoint whenever the
user joined without editing the pre-filled name.

Fall back to user.full_name when the store username is missing, so
the endpoint always receives a value.

Acknowledged as a somewhat wobbly fix, but ships as-is until the
underlying flow is reworked.
2026-08-04 19:04:12 +02:00
lebaudantoine 187e289548 (frontend) close admin side panel when the user is demoted
Listen to role changes in the admin panel and close the side panel
if the current user is demoted while it is open. Without this,
unprivileged users could still see the admin side panel until they
closed it manually.

I checked the other features that could be affected by hot role
changes; this was the only one still exposing admin-only UI after a
demotion. Everything else already handles live permission updates
correctly.
2026-08-04 19:04:12 +02:00
lebaudantoine 496a192ced (frontend) allow promoting authenticated participants
Introduce a new feature that lets a user promote one of the
authenticated participants of the meeting to a role with additional
privileges.

Known limitations:

* Only authenticated participants can be promoted, but there is no
  visual indicator yet distinguishing authenticated from anonymous
  participants. This will be added in a follow-up commit.
* The resource_access data fetched in the initial API call becomes
  stale after a promotion. It is not currently used in the product,
  so this is not visible, but it should either be refreshed later
  or removed from the initial fetch.
* Demoting a promoted user turns them into a member, which is still
  a privileged role. This is a deliberate choice until we introduce
  finer-grained tuning of participant roles.
2026-08-04 19:04:12 +02:00
lebaudantoine 715c18d276 ♻️(frontend) extract closeSidePanel action at the store level
Extract the logic that closes the side panel into a utility function
declared at the store module level, as recommended by Valtio.

This avoids re-creating the function on every render and prevents
extra re-renders in components that use it.
2026-08-04 19:04:12 +02:00
lebaudantoine dd3f23b1bf 🐛(backend) allow any string as sub in the API serializer
The API serializer was too restrictive on the `sub` field, expecting
a UUID. This worked in our development and production setups because
our Keycloak is configured to emit UUID subs, but it broke for other
providers.

Per the OIDC spec and the DB model, `sub` can be any string. Align
the serializer with this and accept arbitrary string values.

Fixes #1525.
2026-08-04 19:04:11 +02:00
lebaudantoine 066cd5f704 💄(frontend) render Avatar initials in uppercase
Uppercase the initials rendered in the Avatar so their vertical
centering stays consistent.

With lowercase letters, the initials were slightly shifted toward
the bottom of the Avatar, which broke the alignment.
2026-08-04 19:04:11 +02:00
lebaudantoine 19dfa26ee4 🔥(frontend) remove leftover console.count call
Drop a stray console.count call that was accidentally committed in a
previous PR and had been left in the codebase.
2026-08-04 19:04:11 +02:00
lebaudantoine 82aafa2030 ♻️(frontend) derive is_administrable from participant metadata
The is_administrable flag was previously read from the room API
response through the room serializer, giving the frontend static
information about the user's rights.

Refactor the frontend so it derives this flag from the participant
role carried in the participant metadata instead.

Two benefits:

* The flag now updates live along with the participant
  attributes/metadata, so role changes are reflected immediately.
* It removes the duplication between the API response and the
  metadata, which both used to determine the user's capabilities.
2026-08-04 19:04:11 +02:00
lebaudantoine bfb02b2242 (frontend) add client for the update participant role endpoint
Add the frontend client that calls the update participant role
endpoint. Straightforward API call, no special handling.
2026-08-04 19:04:11 +02:00
lebaudantoine ec9cd84cf8 (backend) expose is_authenticated in the LiveKit token
Include the is_authenticated flag on the user in the LiveKit token
and participant metadata.

The frontend needs this information (used in the next commit) to
know whether it can offer to promote a user with access to the room
admin.
2026-08-04 19:04:10 +02:00
lebaudantoine 65172447ca ♻️(backend) pass the participant role in the LiveKit token
The backend previously passed an abstract is_admin_or_owner boolean
flag in the LiveKit token. That kept the frontend minimalistic and
saved it from having to handle role comparisons.

As we introduce more features that need to distinguish between the
room owner and admins, refactor the token to carry the role
directly. The frontend can then derive the relevant flags from a
richer piece of information.
2026-08-04 19:04:10 +02:00
lebaudantoine 71d76abc0f (backend) add endpoint to update a participant role during a meeting
Add an endpoint that allows updating a user's role while in a
meeting. The goal is to let users promote other connected
participants to admin or moderator, so the burden of administrating
a meeting can be shared.
2026-08-04 19:04:10 +02:00
lebaudantoine 47f3da4153 ️(backend) add permission class checking the user is in the call
Introduce a new permission class that verifies the caller making a
request is both authenticated and actually present in the call.

It will be used to gate actions that require the user to be live in
the room, for example:

* allowing someone in from the waiting room
* promoting another participant to a different role

More generally, this covers every action where, for security
reasons, we need to make sure the user is truly present in the call
and that someone is not reusing their cookie as an API key.
2026-08-04 19:04:10 +02:00
lebaudantoine 6dd914fe66 📝(frontend) add changelog entry for PR #1510
Document in the CHANGELOG the set of changes shipped in PR #1510,
which groups the recent chat, layout and participant tile render
optimizations.
2026-08-04 19:04:09 +02:00
lebaudantoine a52ea119ac 🐛(frontend) reset chat state when the ChatProvider mounts
Reset the chat state on the first render of the ChatProvider, to
make sure no chat messages from a previous room leak into the new
one.

This covers SPA navigations where the user switches from one room
to another without a full page reload.
2026-08-04 19:04:09 +02:00
lebaudantoine fc4ee70265 🐛(frontend) fix pinnedTrackRef always evaluating to true
pinnedTrackRef was always truthy when evaluated in this
code path.
2026-08-04 19:04:09 +02:00
lebaudantoine 81f23aeb88 ️(frontend) tripwire promotion of off-screen active speakers
Introduce a tripwire component that listens to
RoomEvent.ActiveSpeakersChanged imperatively and forces a single
re-render of its host only when an active speaker has none of their
tiles within the visible span (maxVisibleTiles). That re-render
re-runs useVisualStableUpdate, which reads live isSpeaking state
and performs the actual tile swap.

Speakers already visible are ignored, so this costs zero React work
in the common case.

This lets us drop the ActiveSpeakersChanged subscription from
useTracks in the StageLayout upstream (updateOnlyOn: []), which was
re-rendering the whole stage on every speaker change.
2026-08-04 19:04:09 +02:00
lebaudantoine a5651249c5 ️(frontend) memoize the EffectsButton
Memoize the EffectsButton so it does not re-render on unrelated
parent updates when its props have not changed.
2026-08-04 19:04:08 +02:00
lebaudantoine 167839ebd0 ️(frontend) reduce re-renders of the ParticipantTile focus overlay
Optimize the idle mouse handling and the FocusOverlay component so
they no longer trigger frequent re-renders of the ParticipantTile
focus.

State related to hover and focus is now scoped closer to where it
is used, keeping updates local instead of propagating up the tile.
2026-08-04 19:04:08 +02:00
lebaudantoine 18ec920ebe 🚚(frontend) extract ParticipantTile sub-components into their own files
Split the sub-components currently declared inside ParticipantTile
into dedicated files.

This makes the ParticipantTile file easier to read and lets each
sub-component be imported and reasoned about on its own.
2026-08-04 19:04:08 +02:00
lebaudantoine 8a24502295 ♻️(frontend) harmonize participant name handling in ParticipantTile
Align how the participant name is retrieved and rendered inside the
ParticipantTile, so the different code paths use a single consistent
approach instead of a mix of ad hoc logic.
2026-08-04 19:04:08 +02:00
lebaudantoine c3f212bf54 ️(frontend) synchronize room metadata in a leaf component
Wrap the hook in a leaf component.
2026-08-04 19:03:18 +02:00
lebaudantoine a0274a2d54 ️(frontend) memoize the Placeholder component
Turn the Placeholder into a pure leaf component and memoize it, so
it does not re-render on unrelated parent updates when its props
have not changed.
2026-08-04 19:02:40 +02:00
lebaudantoineandaleb_the_flash 13c9d3635c ️(frontend) size the Avatar with CSS instead of useSize
Stop using the useSize hook to compute the Avatar size in JS. Rely
on CSS to size the Avatar responsively instead.

This removes a set of unnecessary re-renders triggered by the
useSize subscription every time the container resized.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash eced9891c6 ️(frontend) optimize re-renders of the CarouselLayout
Apply the same pattern as previous layouts: push state and useSize
subscriptions down into child components.

The CarouselLayout now only re-renders when maxVisibles or
orientation actually change, instead of on every size update.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash ef05c0ab19 ️(frontend) isolate useSize in a child of MoreOptions
Move the useSize subscription into a dedicated child component of
MoreOptions.

The options now only re-render when they actually need to collapse,
instead of on every size change of the container.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 932332c858 ️(frontend) isolate useSize subscription in a GridLayout leaf
Move the useSize observer into a leaf component of the GridLayout
tree. That leaf then propagates size changes back into the
GridLayout state through a callback.

This prevents every size change from re-rendering the whole
GridLayout tree, keeping the re-render local to the leaf that
actually observes the size.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 7343560a9b ️(frontend) reduce unnecessary re-renders in the participant panel
Cut down the number of unnecessary re-renders happening in the
participant panel by narrowing subscriptions and isolating state to
the components that actually depend on it.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash fca7c3b8b8 🐛(frontend) fix long-standing initials centering in Avatar
The Avatar was rendered as an image, which made the initials
centering unreliable across sizes and browsers.

Render the Avatar as an SVG instead, which allows the initials to be
properly centered by construction.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 29672cda6b ️(frontend) gate LowerAllHandsButton with AdminOrOwnerOnly
Align LowerAllHandsButton with the MuteEveryoneButton pattern by
wrapping it with the AdminOrOwnerOnly component.

This prevents mounting its hooks and rendering its logic for users
who are not admin or owner, instead of relying on an internal check
that still ran on every re-render.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash c3c1e918b0 ️(frontend) extract participant count into a dedicated component
Extract the participant count out of the participant badge into a
dedicated, optimized component.

This isolates the subscription to the participant count so it no
longer triggers a re-render of the whole toggle when the count
changes.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash d1dde71bea 🚚(frontend) move participant list code into a feature folder
Reorganize all participant list related code elements into a
dedicated feature folder, so related components, hooks and helpers
are grouped together and easier to locate.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 64b4200fc3 ♻️(frontend) major chat refactoring
- Dissociate the chat observer (which persists messages in a store)
  from chat rendering.
- Do not keep the chat mounted in the DOM anymore.
- Recompute the minimal amount of data when a participant renames.
- Memoize most of the layout.
- Drop the manual textarea row-size computation: recent React Aria
  already handles it.

Known regressions still to solve: focus behavior on the input,
scroll behavior on message updates, and edge cases around list
sizing.

Not sure yet whether there is a better way to memoize the
virtualized list; open to feedback.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 4b393e28ca ️(frontend) memoize the ActiveSpeaker component
Memoize the ActiveSpeaker component used in the push-to-talk
component so it does not re-render on unrelated parent updates when
its props have not changed.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 8dc1bacc38 🚚(frontend) move chat-related code into a feature folder
Reorganize all chat-related code elements into a dedicated feature
folder, so chat components, hooks and helpers are grouped together
and easier to locate.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 97a9735c0b ️(frontend) avoid subscribing to full local participant changes
Subscribing to the whole local participant caused re-renders on
every local participant update, even when the consumer only cared
about a specific attribute.

Narrow the subscription so only the relevant attributes trigger a
re-render.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash d2e13cc826 ️(frontend) push video resolution subscription down the tree
Move the video resolution subscription down into a lower component
so it no longer re-renders the whole Videoconference component on
every resolution change.

The overall approach still needs validation, but this already avoids
the top-level re-render and is a clear improvement over the current
behavior.i
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 183b0fc697 ️(frontend) narrow canPublishTrack subscription to permissions
canPublishTrack was mistakenly subscribing to the whole local
participant changes, so every component using this hook re-rendered
on any local participant update, not only on permission changes.

Fix it to subscribe only to the permission attributes.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash b7be0eabb0 ️(frontend) render full-screen warning only for the local participant
Restrict the full-screen warning to the local participant tile.

Previously, mounting it on every participant tile meant that when
the local participant toggled their camera or microphone, the
warning re-evaluated and triggered a re-render on every tile.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash f21c4c91ed ️(frontend) optimize participant metadata rendering on raised hand
Push state down into the participant metadata subtree so a raised
hand change no longer re-renders the whole metadata block.

Use the children-as-props pattern so only the item actually related
to the raised hand re-renders when its state changes.

This also better encapsulates the color-update logic tied to the
raised-hand state.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 5f52fa8d8d ️(frontend) minimize observables consumed by useRaisedHandPosition
useRaisedHandPosition is called on every participant tile, so any
unnecessary subscription in it multiplies re-renders across the
whole conference.

Reduce the number of events subscribed to for remote participants
to the strict minimum, while keeping the subscription to the
relevant attributes of the local participant intact.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 9420fed563 ️(frontend) encapsulate participant metadata in a boundary component
Encapsulate the participant metadata rendering in a clear boundary
component, especially by pushing the raised-hand logic down into a
dedicated child.

This way, when a participant raises their hand, only the child
component re-renders instead of the whole ParticipantTile.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 4598aafa23 ️(frontend) narrow chat event subscriptions to reduce re-renders
Reduce the set of events the chat subscribes to, keeping only those
that actually affect its rendering.

This avoids frequent re-renders triggered by unrelated events on
the room or participants.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 94abdfc0f9 ️(frontend) read pinned track imperatively in ParticipantTile
In the participant tile, there is no need to read the pinned track
through a Valtio snapshot, which would trigger a re-render every
time the pinned track changes, for every participant tile.

Switch to an imperative read of the store to avoid these unnecessary
re-renders.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 56a8f72e39 🚚(frontend) move participant tile components into a feature folder
Reorganize all components related to the participant tile into a
dedicated feature folder, so the code organization is clearer and
related components are grouped together.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 86fcc2c7fe ♻️(frontend) drop useSettingsDialogs hook
The useSettingsDialogs hook only encapsulated Valtio store
manipulation that should be declared at the module level. Keeping it
as a hook triggered unnecessary re-renders in components that used
it, subscribing to the store.

Remove the hook and use the store actions directly at the module
level.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 0a0306b0a2 ♻️(frontend) move keyboard shortcut registration to a leaf
Move the keyboard shortcut registration down into the
SettingsDialogProvider leaf component, so shortcut-related
re-renders no longer bubble up and re-render the whole
Videoconference component.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash c00fcc78e1 ️(frontend) narrow participant event subscriptions in admin panel
Reduce the set of participant change events the admin side panel
subscribes to, so it only re-renders on events that actually affect
its display.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash 39a59d1b35 ️(frontend) narrow ParticipantToggle event subscriptions
Reduce the set of events the ParticipantToggle subscribes to, so it
does not re-render on every LocalParticipant event but only on the
ones that actually affect its rendering.
2026-07-24 18:31:47 +02:00
lebaudantoineandaleb_the_flash f4493d412f ♻️(frontend) turn ConnectionObserver hook into a leaf component
Refactor the connection observer, previously a hook consumed inside
the Videoconference component, into a dedicated leaf component in
the tree.

This avoids re-rendering the whole Videoconference component every
time the connection state changes; only the leaf reacts to it.
2026-07-24 18:31:47 +02:00