feat(ui): call controls - #1324
Open
renefloor wants to merge 25 commits into
Open
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks 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 |
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
2 times, most recently
from
September 5, 2026 12:00
2aa6f20 to
e69789a
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 08:03
e69789a to
ca06469
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 10:26
ca06469 to
847d9d7
Compare
renefloor
commented
Sep 8, 2026
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 12:28
847d9d7 to
cb26ab9
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 13:17
cb26ab9 to
ac4da35
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 13:22
ac4da35 to
2595a18
Compare
renefloor
force-pushed
the
feat/call-controls
branch
from
September 8, 2026 13:45
b5918a2 to
17ec975
Compare
renefloor
marked this pull request as ready for review
September 8, 2026 14:02
This was referenced Sep 9, 2026
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>
A camera or microphone the call refuses at join left the intent set to on. The setters only downgrade connectOptions on success, so a control reading the intent while no track had been reported drew the device as live for the rest of the call, and could not toggle it — there was no track to mute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Export device_control.dart; the changelog already announced trackEnabled and wantsOn, which no consumer could reach. - StreamLayoutButton takes `layout` and keeps no state. Every caller already owned the mode, and the button ignored later changes to it. - CallControlBar takes `small` by name, with super.key last. - heightOf includes the bottom inset a primary bar clears, so a caller insetting content out from under a floating bar no longer falls short. - Drop resolveSurfaceStyle, which had no callers, and share the one style-resolution chain the three readers had copied. - onError takes a StreamDeviceErrorCallback: a typed VideoError plus the action that failed, so a caller names the action instead of guessing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The closure ran to seventy lines with three inline layouts. Two of its comments were also wrong: the settings menu keeps its own device notifier rather than this screen's controller, and the tablet layout adds one control, not two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`reportsNo` is true whether the platform has no such device or could not list one at all, so a control badging itself off it could not say whether a retry was worth offering. `enumerationFailed` reads the reason the controller already records. Also corrects the `devices` docs: the split buttons always enumerate, building a controller for the call when none is passed, and reading the error or retrying needs a controller the screen holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every one of these fails against a deliberate fault and passed before: - StreamCameraButton had no test of its own. Pointing it at the microphone's device list and join intent left the suite green. - StreamMicrophoneSplitButton's call-driven path was never instantiated; the camera tests give it nothing, the two being separate implementations. - stopTrackOnMute reached no test at all. - primary/SafeArea reached none either, and the golden cannot help: its MediaQuery replaces the whole data object, zeroing the inset. - The bar's layout choice was tested only through layoutFor, so hard-coding a screen size in build() passed every widget test. - 'builds and disposes a controller of its own' asserted takeException is null, which a leak also satisfies. It now watches the device subscription, which a leak keeps alive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Three docs still promised a hairline along the bar's top edge, which 2e3f253 removed; the bar draws no border at all. - The devices doc claimed the split button opts into enumeration the same way. It always enumerates, building a controller when passed none. - The onError doc justified an untyped error by an analogy to enumerationError, which is a StreamDeviceError, and by VideoError being unnameable outside stream_video, which exports it. - withDevices does no muting; the caller's onPressed does. - selectedAudioInput led with the null case rather than the behaviour. - Eight controls still opened with "a call control option". - The speakerphone button logged under SV:SpeakerphoneOption. - default_control_options.dart suppressed a deprecation it no longer has. - Dropped the refactor narration the house style keeps out of doc comments, and documented the two undocumented screen-share fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new entries ran to several paragraphs and a code sample each, and two carried claims the code does not make: a hairline along the bar's top edge, and a height that varies with the window when it is one themed value at every breakpoint. Entries inherited from feat/lobby-view are left as they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
renefloor
force-pushed
the
feat/call-controls
branch
from
September 9, 2026 08:56
4d70250 to
111f078
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