feat(ui): call controls - #1324
Draft
renefloor wants to merge 16 commits into
Draft
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feat/lobby-view #1324 +/- ##
==================================================
Coverage ? 24.80%
==================================================
Files ? 372
Lines ? 28140
Branches ? 0
==================================================
Hits ? 6979
Misses ? 21161
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
renefloor
force-pushed
the
feat/call-controls
branch
from
September 3, 2026 08:54
ffd591a to
1b71546
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 4, 2026 15:21
2e38aef to
2aa6f20
Compare
StreamLobbyController fetched with getOrCreate, so the call came into existence the moment the lobby was shown — inherited from the lobby view this branch replaced, where the same call sat in build(). That is a decision the lobby is not entitled to make. What a call is created with is fixed at creation, its encryption mode above all, and dogfooding deliberately defers creation to the join press so the encryption switch is still live when getOrCreate is finally called. It never was: the controller had already created the call, unencrypted, before the switch was drawn, and the create-on-join then returned that existing call. Starting an encrypted call produced a plain one. Joining an existing encrypted call was unaffected, because there the fetch is only ever a fetch. The controller reads the call now. get() runs the same _performGetOperation and returns the same metadata, so nothing else changes; a call that does not exist yet fetches as a failure, which the controller already handles — the lobby keeps its preview, lists no participants, and reports fetchError. Creating is the host's, before the lobby or from onJoinCallPressed. The example's join tab typed an id straight into the lobby and relied on it being created, so it now says so itself. CallReceivedData, what Call.get() returns, was not exported, so no caller could name the type it has to handle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A row of controls along the bottom of a call, taking one CallControlBarLayout per screen size and drawing the one the window calls for. Only `small` is required; `medium` and `large` fall back to the next smaller layout that was given. The centre slot is centred in the bar's full width rather than in the gap between the two sides, so a lopsided leading group leaves it where it was. The bar owns its chrome — background, top hairline, safe area, and a docked or floating surface — but no controls: the caller supplies every one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hand-rolled Container + SafeArea + Row + Spacer with the bar, which is the split it was already faking. A wide window gets the full design instead: closed captions, reactions, recording and leaving in a centred row, settings and the layout toggle leading, stats and the participant and chat panels trailing. The layout toggle drops out of the app bar past the large breakpoint, where it lives in the bar's leading slot instead. Picture-in-picture is the one control in the design with nothing to wire to: the SDK only enters it when the app backgrounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar was taking core's chrome colour, `backgroundElevation1`, which is what StreamBottomAppBar and StreamAppBar use. The call screen does not: CallAppBar and the call content's scaffold are both `backgroundApp`. Lifting only the bottom bar left the two ends of one call screen a shade apart in dark mode. Light mode paints all three elevations white, which is why the design frames and the light goldens agreed either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Another thing carried over from StreamBottomAppBar that the call screen does not want. Core's bars treat a `borderSubtle` line along the top edge as part of their identity because they separate one page of content from another; a call has nothing to separate the controls from, and the design frames draw no line — their top row of pixels is the fill colour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The control bar's centre row already ends in LeaveCallOption there, so the screen offered two ways to hang up a few hundred pixels apart. Same reasoning as the layout toggle, and now the same flag drives both. Below the breakpoint it stays: the phone bar has no leave button — the design does not give it one — so the app bar is the only way out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Android bar carried six controls where the design draws five, and its microphone and camera were split buttons with a caret each. On a phone that is a device picker nobody has room to aim at: one microphone, two cameras. Screen sharing and the pickers stay reachable from the more menu. Medium takes what small used to be, rather than falling back to it. A tablet has the width for screen sharing and a caret on each device, and changing microphone mid-call without opening a menu is worth two extra controls at that size. The phone's toggles are plain CallControlButtons rather than the SDK's ToggleMicrophoneOption / ToggleCameraOption: those drop the Result that setMicrophoneEnabled returns, which is the silent failure this app already works around. An unavailable device is badged and disabled, not painted destructive — being absent is not a user choice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ToggleMicrophoneOption` and `ToggleCameraOption` no longer drop the `Result` their setter returns. A refusal is logged, and reported to a new `onError`. Dropping it made a refused press invisible: a control's state comes from the call's own participant state, which does not change when the call says no, so a viewer without `sendAudio` got no movement, no message and no log. The split buttons and the participants control now take the call in their default constructor, matching `ToggleScreenShareOption` and the rest of this package. Each does the wiring every call site was repeating: reading the call's device state, toggling it, owning a device controller, and disabling itself when the platform names no such device. Pass `devices` to share one controller with a screen's other pickers. The old shapes are named constructors — `withDevices` and `forParticipants` — for callers that own the state and the action. That is what a lobby needs, where nobody has joined and there is no call state to read, and it is what the `StreamLobby*` wrappers use. None of this has shipped, so there is nothing to deprecate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The call screen's device helpers were the wiring that has moved into the SDK: folding the setter's Result, computing whether the platform reported a device, and mapping participants to UserInfo. What is left is the part that is genuinely this app's — a snackbar on refusal, `onTap` opening its own participants screen, and the shared device controller the settings menu also reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ToggleMicrophoneOption` is `StreamMicrophoneButton`, and the other nine controls follow: camera, screen share, recording, closed captions, layout, speakerphone, flip camera, add reaction and leave call. The family now matches the split buttons, the CallControlButton / CallFeatureButton primitives, and the design system's own components. "Toggle" went with the prefix — it described the press rather than the control, and half the family never toggled anything. Every old name survives as a deprecated typedef, and a typedef to a class carries its constructors, so an unmigrated call site keeps compiling whatever arguments it passes. fix_data.yaml renames all ten, verified with `dart fix --apply` against a file using the old names. Alongside it, the microphone and camera buttons take an optional `devices` and, given one, badge themselves and stop responding while the platform reports no such device — neutral rather than the negative tone a deliberate mute gets, since an absent device is not a user choice. Only the split buttons could say this before, so a narrow control bar built from the plain toggles lost it. Optional because a plain toggle needs no controller, and enumerating devices to draw one is a cost a screen should opt into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the SDK rename, and hands the phone bar's plain microphone and camera buttons this screen's device controller so a device the platform does not report is badged and inert there too — it was, before that bar swapped its split buttons for plain toggles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CallParticipantState.isAudioEnabled` is `!(audioTrack?.muted ?? true)`, so a track the SFU has not named yet reads exactly like one the user muted. Every microphone and camera control drew red with a slash for the second between joining and the first track arriving. The two are distinguishable: `_toggleTrackType` keeps the track entry and flags `muted` rather than dropping it, so an absent entry means nothing has reported the track. New `CallParticipantState.trackEnabled` returns null for that case, and while it is null a control draws the intent the call was joined with — `TrackOption.wantsOn`, where a provided track counts as on, since the lobby only hands one over for a device it opened. A reported track always wins, so muting a call joined unmuted sticks. The intent is read only when the track is unreported, so a control whose track has arrived never reaches for the call's options. The existing tests stubbed `isAudioEnabled` directly; they stub `publishedTracks` now, which is what production derives it from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar is `kStreamToolbarHeight` (72) tall with the same edge padding at every breakpoint. `CallControlBarStyle`'s `compactHeight` / `expandedHeight` and `compactPadding` / `expandedPadding` collapse into `height` and `padding`. Changing geometry across a breakpoint made the bar jump as a desktop window was dragged over 1280, which read as a glitch rather than as a layout. Only which controls are drawn varies by size now, which is the distinction that earns its keep. The padding default is `spacing.sm` (12) rather than the design's 16: a control is 40 visible inside a 48 tap target, which contributes the other 4 on every edge. That lands the visible inset on 16 horizontally and vertically at once — the same pair of values StreamBottomAppBar uses at the same height — and a test pins the rendered inset rather than only the token behind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CallFeatureTone.destructive` paints a feature red while it is on instead of accent blue, and `StreamRecordingButton` uses it: a running recording is capturing the call, which the design marks out from a feature that is merely active. Only the selected state changes. An unselected feature stays the same secondary grey whatever its tone, matching the design system component across its Captions, Screen Share, Picture in Picture and Record variants — and a test pins that, so a row of features keeps reading as one row until something is switched on. The red needs no new colour: `StreamButtonStyle.destructive` resolves to `accentError`, 0xFFD90D10, which is the pixel the design draws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No in-call device menu has ever shown a selection, on any platform. `StreamMediaDevicesController.forCall` reports `supportsSystemDefault: false` — a call's setters take a device, so there is no row for "let the platform pick" — and the selection stayed null until something was picked, so `_options` gave every row `selected: false`. The lobby escaped it because null has a row there. The control bar's split buttons are the first in-call menus, which is why it surfaced now. Where null cannot be drawn, the selection getters resolve to the device the platform reports as its own: the entry under the reserved `default` id, which web lists as "Default - <name>" beside the real devices. There it is a device like any other, so the row is selectable, and `_select` now compares against the resolved value so picking it applies nothing — the platform is already using it. A platform naming no such entry is unchanged. Nothing in the SDK records what the platform picked for itself: `CallState.audioInputDevice` and a local track's `sourceDevice` are both written only by an explicit `setAudioInputDevice`. Reading the browser's own answer off `getSettings()` would cover the microphone but not the speaker, which has no track to ask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
renefloor
force-pushed
the
feat/call-controls
branch
from
September 5, 2026 12:00
2aa6f20 to
e69789a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Goal
Making new call control bar
🛠 Implementation details
This adds a new responsive call controls bar with no controls by default.
🎨 UI Changes
Screen.Recording.2026-09-03.at.09.49.31.mov
☑️Contributor Checklist
General
☑️Reviewer Checklist