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.
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.
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.
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.
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.
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.
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.
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.
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.
Updating room access rewrote the entire metadata payload, removing information
about active recordings. This caused the frontend to lose track of ongoing
recordings and could trigger 409 errors when attempting to start a new
recording.
Consolidate the duplicated metadata update logic into
`RoomManagement.update_metadata()` and preserve merge behavior instead of
overwriting the full metadata object.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
Reorganize all chat-related code elements into a dedicated feature
folder, so chat components, hooks and helpers are grouped together
and easier to locate.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.