From dcf4137c2482428bced9e49191ef0054ada5fc7d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 13:12:45 +0200 Subject: [PATCH 01/11] feat(ui): add CallRingingButton, the 64px answer and decline button The ringing designs give answering and declining a button no StreamButtonSize names. CallControlButton gains a themeStyle pass-through to carry the size: it resolves after the ambient theme, where a nested StreamButtonTheme would be dropped for the positive tone, which already brings one of its own. Co-Authored-By: Claude Opus 5 --- .../call_controls/call_control_button.dart | 10 ++ .../call_controls/call_ringing_button.dart | 95 ++++++++++++++++ .../widgets/avatar_size_from_constraints.dart | 11 +- .../lib/stream_video_flutter.dart | 1 + .../call_ringing_button_test.dart | 107 ++++++++++++++++++ 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart create mode 100644 packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart diff --git a/packages/stream_video_flutter/lib/src/call_controls/call_control_button.dart b/packages/stream_video_flutter/lib/src/call_controls/call_control_button.dart index 84e9d1831..38e4dd1f3 100644 --- a/packages/stream_video_flutter/lib/src/call_controls/call_control_button.dart +++ b/packages/stream_video_flutter/lib/src/call_controls/call_control_button.dart @@ -46,6 +46,7 @@ class CallControlButton extends StatelessWidget { this.showErrorBadge = false, this.onPressed, this.tooltip, + this.themeStyle, }); /// The icon of the button. @@ -70,6 +71,14 @@ class CallControlButton extends StatelessWidget { /// The message shown when the button is long-pressed or hovered. final String? tooltip; + /// Overrides handed to this button alone. + /// + /// Resolved after the ambient [StreamButtonTheme], so it reaches this button + /// without restyling every other control around it. A nested + /// [StreamButtonTheme] would not compose the same way: only the nearest one + /// is read. + final StreamButtonThemeStyle? themeStyle; + @override Widget build(BuildContext context) { final button = StreamCallButtonBadge( @@ -78,6 +87,7 @@ class CallControlButton extends StatelessWidget { icon: icon, onPressed: onPressed, tooltip: tooltip, + themeStyle: themeStyle, style: switch (tone) { .positive => .primary, .neutral => .secondary, diff --git a/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart b/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart new file mode 100644 index 000000000..757293be4 --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../stream_video_flutter.dart'; + +/// The oversized round button a ringing screen is built on: answering, +/// declining, and hanging up a call that has not been picked up yet. +/// +/// It is the same button as [CallControlButton] — same tones, same colours — +/// drawn at the size the ringing designs give it, which no [StreamButtonSize] +/// names. Use [CallControlButton] for the controls that sit alongside it, and +/// anywhere inside a call. +/// +/// {@tool snippet} +/// +/// ```dart +/// CallRingingButton( +/// icon: Icon(context.streamIcons.phoneFill), +/// tone: .positive, +/// label: 'Accept', +/// onPressed: call.accept, +/// ) +/// ``` +/// {@end-tool} +class CallRingingButton extends StatelessWidget { + /// Creates a new instance of [CallRingingButton]. + const CallRingingButton({ + super.key, + required this.icon, + required this.tone, + this.label, + this.onPressed, + this.tooltip, + }); + + /// The diameter of the button. + /// + /// Already well past the minimum tap target, which is why the button drops + /// the padding Material would otherwise add around it. + static const double diameter = 64; + + /// The size of the icon the button carries. + static const double iconSize = 32; + + /// The icon of the button. + final Widget icon; + + /// What pressing this button means. + final CallControlTone tone; + + /// The text under the button, or null to leave it unlabelled. + final String? label; + + /// The callback to invoke when the user taps on the button. + /// + /// Null renders the button disabled. + final VoidCallback? onPressed; + + /// The message shown when the button is long-pressed or hovered. + final String? tooltip; + + @override + Widget build(BuildContext context) { + final button = CallControlButton( + icon: icon, + tone: tone, + onPressed: onPressed, + tooltip: tooltip, + themeStyle: StreamButtonThemeStyle.from( + fixedSize: const Size.square(diameter), + minimumSize: const Size.square(diameter), + iconSize: iconSize, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ); + + if (label case final label?) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + button, + SizedBox(height: context.streamSpacing.sm), + Text( + label, + textAlign: TextAlign.center, + style: context.streamTextTheme.captionEmphasis.copyWith( + color: context.streamColorScheme.textSecondary, + ), + ), + ], + ); + } + + return button; + } +} diff --git a/packages/stream_video_flutter/lib/src/widgets/avatar_size_from_constraints.dart b/packages/stream_video_flutter/lib/src/widgets/avatar_size_from_constraints.dart index 405a9d93a..6c3238415 100644 --- a/packages/stream_video_flutter/lib/src/widgets/avatar_size_from_constraints.dart +++ b/packages/stream_video_flutter/lib/src/widgets/avatar_size_from_constraints.dart @@ -10,22 +10,23 @@ import '../../stream_video_flutter.dart'; /// avatar never comes out smaller than it was asked to be. /// /// Constraints wider than the largest diameter land on that one instead: there -/// is nothing bigger to round up to, so a legacy theme asking for 100px gets -/// [StreamAvatarSize.xxl] at 80px. +/// is nothing bigger to round up to, so a legacy theme asking for 200px gets +/// the largest diameter the design system has. @internal StreamAvatarSize avatarSizeFromConstraints(BoxConstraints constraints) { final diameter = constraints.constrain(Size.infinite).shortestSide; + final largest = StreamAvatarSize.values.last; // Rounding up is the promise above, and this is the one case it cannot keep: // an app whose avatars quietly got smaller during the migration finds out // from a debug log rather than from a screenshot. Unbounded constraints land // here too, where the largest diameter is the only sensible answer. assert(() { - if (diameter.isFinite && diameter > StreamAvatarSize.xxl.value) { + if (diameter.isFinite && diameter > largest.value) { debugPrint( 'StreamUserAvatar: a deprecated theme asked for ${diameter}px, which ' 'is larger than the biggest design-system avatar ' - '(${StreamAvatarSize.xxl.value}px); it will be drawn at that size.', + '(${largest.value}px); it will be drawn at that size.', ); } return true; @@ -33,6 +34,6 @@ StreamAvatarSize avatarSizeFromConstraints(BoxConstraints constraints) { return StreamAvatarSize.values.firstWhere( (it) => it.value >= diameter, - orElse: () => StreamAvatarSize.xxl, + orElse: () => largest, ); } diff --git a/packages/stream_video_flutter/lib/stream_video_flutter.dart b/packages/stream_video_flutter/lib/stream_video_flutter.dart index 9d7d200b9..7c41894ba 100644 --- a/packages/stream_video_flutter/lib/stream_video_flutter.dart +++ b/packages/stream_video_flutter/lib/stream_video_flutter.dart @@ -18,6 +18,7 @@ export 'src/call_controls/call_control_button.dart'; export 'src/call_controls/call_control_option.dart'; export 'src/call_controls/call_controls.dart'; export 'src/call_controls/call_feature_button.dart'; +export 'src/call_controls/call_ringing_button.dart'; export 'src/call_controls/controls/default_control_options.dart'; export 'src/call_controls/controls/stream_add_reaction_button.dart'; export 'src/call_controls/controls/stream_camera_button.dart'; diff --git a/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart b/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart new file mode 100644 index 000000000..af88d6fc3 --- /dev/null +++ b/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart @@ -0,0 +1,107 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/core.dart'; +import 'package:stream_video_flutter/stream_video_flutter.dart'; + +import '../../test_utils/goldens.dart'; +import '../../test_utils/test_wrapper.dart'; + +void main() { + const icons = StreamIcons(); + + group('CallRingingButton', () { + for (final tone in [CallControlTone.positive, CallControlTone.negative]) { + // `positive` is the interesting one: CallControlButton repaints it + // through a StreamButtonTheme of its own, and only the nearest such + // theme is read — so a size handed down as an outer theme would be + // dropped for this tone alone. + testWidgets('${tone.name} is ${CallRingingButton.diameter}px across', ( + tester, + ) async { + await tester.pumpWidget( + TestWrapper( + child: Center( + child: CallRingingButton( + icon: Icon(icons.phoneFill), + tone: tone, + onPressed: () {}, + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(StreamButton)), + const Size.square(CallRingingButton.diameter), + ); + // The size reaches the icon through ButtonStyle.iconSize, so it is the + // rendered box that shows it, not the Icon widget's own `size`. + expect( + tester.getSize(find.byType(Icon)), + const Size.square(CallRingingButton.iconSize), + ); + }); + } + + testWidgets('a label sits under the button, not beside it', (tester) async { + await tester.pumpWidget( + TestWrapper( + child: Center( + child: CallRingingButton( + icon: Icon(icons.phoneDownFill), + tone: .negative, + label: 'Decline', + onPressed: () {}, + ), + ), + ), + ); + + final button = tester.getRect(find.byType(StreamButton)); + final label = tester.getRect(find.text('Decline')); + + expect(label.top, greaterThanOrEqualTo(button.bottom)); + expect(label.center.dx, moreOrLessEquals(button.center.dx, epsilon: 0.5)); + }); + }); + + for (final brightness in Brightness.values) { + streamGoldenTest( + 'CallRingingButton paints the ringing pair', + fileName: 'call_ringing_button', + brightness: brightness, + builder: () => GoldenTestGroup( + columns: 3, + children: [ + GoldenTestScenario( + name: 'decline', + child: CallRingingButton( + icon: Icon(icons.phoneDownFill), + tone: .negative, + label: 'Decline', + onPressed: () {}, + ), + ), + GoldenTestScenario( + name: 'accept', + child: CallRingingButton( + icon: Icon(icons.phoneFill), + tone: .positive, + label: 'Accept', + onPressed: () {}, + ), + ), + GoldenTestScenario( + name: 'unlabelled', + child: CallRingingButton( + icon: Icon(icons.phoneDownFill), + tone: .negative, + onPressed: () {}, + ), + ), + ], + ), + ); + } +} From 9ea27e1c91bfddd9c8faf3406c7a4187536dfb83 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 13:30:42 +0200 Subject: [PATCH 02/11] feat(ui): redesign the incoming and outgoing ringing screens Both are now an avatar over a name and a status line with the controls below, built on the design system. The incoming screen sits on the app surface; the outgoing one is drawn on the caller's own camera, blurred behind a scrim, opened by a controller that hands the track to the call so answering does not open the camera a second time. The outgoing screen keeps its simpler button layout rather than the design's in-call bars, and the incoming one keeps the microphone and camera toggles the design drops. Co-Authored-By: Claude Opus 5 --- packages/stream_video_flutter/CHANGELOG.md | 13 + .../call_controls/call_ringing_button.dart | 16 +- .../common/calling_participants.dart | 65 ---- .../common/participant_avatars.dart | 139 -------- .../common/ringing_call_background.dart | 60 ++++ .../common/ringing_call_details.dart | 103 ++++++ .../common/ringing_call_style_defaults.dart | 61 ++++ .../incoming_call/incoming_call_content.dart | 156 +++++---- .../incoming_call/incoming_call_controls.dart | 95 +++--- .../outgoing_call/outgoing_call_content.dart | 229 +++++++------ .../outgoing_call/outgoing_call_controls.dart | 69 ++-- .../ringing_camera_controller.dart | 173 ++++++++++ .../src/l10n/arb/stream_video_flutter_en.arb | 37 +++ .../src/l10n/arb/stream_video_flutter_nl.arb | 9 +- .../stream_video_flutter_localizations.dart | 42 +++ ...stream_video_flutter_localizations_en.dart | 31 ++ ...stream_video_flutter_localizations_nl.dart | 31 ++ .../lib/src/theme/components/components.dart | 1 + .../theme/components/ringing_call_theme.dart | 250 ++++++++++++++ .../ringing_call_theme.g.theme.dart | 306 ++++++++++++++++++ .../theme/incoming_outgoing_call_theme.dart | 26 ++ .../lib/src/theme/stream_video_theme.dart | 90 +++++- .../lib/stream_video_flutter.dart | 3 +- .../call_ringing_button_test.dart | 1 - .../src/call_screen/ringing_call_test.dart | 255 +++++++++++++++ .../ringing_camera_controller_test.dart | 150 +++++++++ .../stream_video_flutter/test/src/mocks.dart | 56 ++++ 27 files changed, 1995 insertions(+), 472 deletions(-) delete mode 100644 packages/stream_video_flutter/lib/src/call_screen/common/calling_participants.dart delete mode 100644 packages/stream_video_flutter/lib/src/call_screen/common/participant_avatars.dart create mode 100644 packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart create mode 100644 packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart create mode 100644 packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart create mode 100644 packages/stream_video_flutter/lib/src/call_screen/outgoing_call/ringing_camera_controller.dart create mode 100644 packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.dart create mode 100644 packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.g.theme.dart create mode 100644 packages/stream_video_flutter/test/src/call_screen/ringing_call_test.dart create mode 100644 packages/stream_video_flutter/test/src/call_screen/ringing_camera_controller_test.dart diff --git a/packages/stream_video_flutter/CHANGELOG.md b/packages/stream_video_flutter/CHANGELOG.md index 26d03293e..bfa2faf5f 100644 --- a/packages/stream_video_flutter/CHANGELOG.md +++ b/packages/stream_video_flutter/CHANGELOG.md @@ -19,6 +19,13 @@ - Added `StreamDesktopScreenShareSelectorThemeData` on `StreamVideoTheme`, and `StreamDesktopScreenShareSelectorTheme` to restyle the selector over a subtree. - Added `desktopScreenShareRefresh`, `desktopScreenShareNoSources`, `desktopScreenShareLoadFailed` and `desktopScreenShareRetry` to the localizations, in English and Dutch. - Added `ViewportVisibilityReporter`, which measures how much of its child is on screen and reports it to `Call.viewportVisibility`. +- Redesigned the incoming and outgoing ringing screens. Both are an avatar over a name and a status line, with the call controls below. The incoming screen sits on the app surface; the outgoing one is drawn on top of the caller's own camera, blurred behind a scrim, and falls back to the flat scrim when the camera is off. +- Added `CallRingingButton`, the 64px round button answering, declining and cancelling are drawn with. It is `CallControlButton` at the size the ringing designs give it, optionally with a label under it. +- `CallControlButton` takes a `themeStyle`, handed to the button as `props.themeStyle`. It resolves after the ambient `StreamButtonTheme`, so it reaches that one button — where a nested `StreamButtonTheme` would be dropped for the `positive` tone, which brings one of its own. +- Added `StreamRingingCameraController`, which opens the camera the outgoing screen previews and hands it to the call as `TrackOption.provided`, so the call carries on with the camera the caller was already previewing rather than opening a second one. `StreamOutgoingCallContent` makes one unless it is given one. +- Added `StreamIncomingCallThemeData` and `StreamOutgoingCallThemeData` on `StreamVideoTheme`, with `StreamIncomingCallTheme` and `StreamOutgoingCallTheme` to restyle either over a subtree. Both carry a `StreamRingingCallStyle`, which `StreamIncomingCallContent.style` and `StreamOutgoingCallContent.style` override per call site. +- Added `RingingCallBackground`, the outgoing screen's default background, so a `callBackgroundWidgetBuilder` has something to build on. +- Added ringing strings to the localizations, in English and Dutch: `ringingIncomingCall`, `ringingCalling`, `ringingAccept`, `ringingDecline`, `ringingNobody`, `ringingTwoCallers` and `ringingManyCallers`. The ringing screens' text was hardcoded English. - `StreamLayoutButton` draws the participant layout in effect and offers the rest through a `StreamAdaptiveMenuAnchor`. - `StreamLayoutButton.defaultLayouts` is `auto` and `speakerBottom`, so the button toggles unless it is given more. - Added layout strings to the localizations, in English and Dutch: `layoutMenuTitle`, `layoutSelectTooltip`, `layoutDefault`, `layoutGrid`, `layoutSpeakerTop`, `layoutSpeakerBottom`, `layoutSpeakerLeft`, `layoutSpeakerRight` and `layoutSpeakerOneToOne`. @@ -235,6 +242,8 @@ ### ⚠️ Deprecated +- `StreamIncomingOutgoingCallThemeData`, `StreamIncomingOutgoingCallTheme` and `StreamVideoTheme.incomingCallTheme` / `outgoingCallTheme` are deprecated in favour of `StreamIncomingCallThemeData` and `StreamOutgoingCallThemeData`. The ringing screens are built on the design system now and no longer read them, so a theme set through them has no effect. The old pair also shared one inherited widget, so the two screens could never be themed apart. + - `ParticipantLayoutMode.spotlight` is `speakerTop` now, and `pictureInPicture` is `speakerOneToOne`. `dart fix --apply` migrates both. - `StreamLocalVideoThemeData`, `StreamLocalVideoTheme` and `StreamVideoTheme.localVideoTheme` are deprecated in favour of `StreamFloatingParticipantTileThemeData`. `StreamLocalVideo` only positions the self-view now, so nothing reads them. - Every call control is named `StreamButton` now, matching the split buttons, the `CallControlButton` / `CallFeatureButton` primitives, and the design system's own components. `ToggleMicrophoneOption` is `StreamMicrophoneButton`, and alongside it `ToggleCameraOption`, `ToggleScreenShareOption`, `ToggleRecordingOption`, `ToggleClosedCaptionsOption`, `ToggleLayoutOption`, `ToggleSpeakerphoneOption`, `FlipCameraOption`, `AddReactionOption` and `LeaveCallOption` become `StreamCameraButton`, `StreamScreenShareButton`, `StreamRecordingButton`, `StreamClosedCaptionsButton`, `StreamLayoutButton`, `StreamSpeakerphoneButton`, `StreamFlipCameraButton`, `StreamAddReactionButton` and `StreamLeaveCallButton`. @@ -252,6 +261,10 @@ - `ScreenShareThumbnailWidget` is `StreamDesktopScreenShareThumbnail` now, and takes the thumbnail from the source it is given rather than subscribing for one. - `StreamVideoRenderer` is a `StatelessWidget` and wraps its child in a `ViewportVisibilityReporter`, which does the measuring. - `CallParticipantsSpotlightView.padding` is a nullable `EdgeInsetsGeometry?` rather than a non-nullable `EdgeInsets`, and falls back to `StreamCallParticipantsSpotlightStyle.padding` (8px horizontal) when it is null. Code reading the field needs to handle null; code passing one is unaffected. A new `spacing` sets the gaps. +- `StreamIncomingCallContent` and `StreamOutgoingCallContent` no longer take `singleParticipantAvatarTheme`, `multipleParticipantAvatarTheme`, `singleParticipantTextStyle`, `multipleParticipantTextStyle` or `callingLabelTextStyle`. The redesigned screens style one name and many the same, and size a group avatar on its own scale, so there is nothing left for a separate "multiple" theme to reach. Pass a `StreamRingingCallStyle` through `style:` or the new themes instead. +- More than one person ringing is drawn as a `StreamAvatarGroup` rather than two or three separate avatars, and named "A, B, and N others" rather than "A, B and +N more". +- The outgoing screen reads its camera state from `StreamRingingCameraController` rather than from `CallConnectOptions.camera.isEnabled`, which is false for a provided track. + - `ParticipantLayoutMode.auto` is the default layout of `StreamCallContent`, `StreamCallParticipants` and `RegularCallParticipantsContent`, and renders what `grid` used to. The livestream widgets still default to `grid`. - `ParticipantLayoutMode.grid` gives the local participant a tile of its own instead of floating them over the grid. - `ParticipantLayoutMode.auto` floats the self-view on mobile only while at most two other people are in the call, and gives the local participant a tile beyond that. diff --git a/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart b/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart index 757293be4..e8362bdb6 100644 --- a/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart +++ b/packages/stream_video_flutter/lib/src/call_controls/call_ringing_button.dart @@ -28,6 +28,7 @@ class CallRingingButton extends StatelessWidget { required this.icon, required this.tone, this.label, + this.labelStyle, this.onPressed, this.tooltip, }); @@ -50,6 +51,13 @@ class CallRingingButton extends StatelessWidget { /// The text under the button, or null to leave it unlabelled. final String? label; + /// The style of [label]. + /// + /// Defaults to `textTheme.captionEmphasis` in `colorScheme.textSecondary`, + /// which is text on a surface. A button placed on top of video or an image + /// needs a color that reads there instead. + final TextStyle? labelStyle; + /// The callback to invoke when the user taps on the button. /// /// Null renders the button disabled. @@ -82,9 +90,11 @@ class CallRingingButton extends StatelessWidget { Text( label, textAlign: TextAlign.center, - style: context.streamTextTheme.captionEmphasis.copyWith( - color: context.streamColorScheme.textSecondary, - ), + style: + labelStyle ?? + context.streamTextTheme.captionEmphasis.copyWith( + color: context.streamColorScheme.textSecondary, + ), ), ], ); diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/calling_participants.dart b/packages/stream_video_flutter/lib/src/call_screen/common/calling_participants.dart deleted file mode 100644 index 5d97bd704..000000000 --- a/packages/stream_video_flutter/lib/src/call_screen/common/calling_participants.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../stream_video_flutter.dart'; - -/// Displays call participants as a string. -class CallingParticipants extends StatelessWidget { - /// Creates a new instance of [CallingParticipants]. - const CallingParticipants({ - super.key, - required this.participants, - this.singleParticipantTextStyle = const TextStyle( - fontSize: 28, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - this.multipleParticipantTextStyle = const TextStyle( - fontSize: 20, - color: Colors.white, - ), - }); - - /// The list of participants in the call. - final List participants; - - /// Text style for the participant label in a call with one participant. - final TextStyle singleParticipantTextStyle; - - /// Text style for the participant label in a call with multiple participants. - final TextStyle multipleParticipantTextStyle; - - @override - Widget build(BuildContext context) { - TextStyle? textStyle; - if (participants.length > 1) { - textStyle = multipleParticipantTextStyle; - } else { - textStyle = singleParticipantTextStyle; - } - - return Center( - child: Text( - _buildParticipantsText(), - style: textStyle, - textAlign: TextAlign.center, - ), - ); - } - - String _buildParticipantsText() { - final length = participants.length; - - if (participants.isEmpty) { - return 'No participants'; - } else if (length == 1) { - return participants[0].name; - } else if (length == 2) { - return '${participants[0].name} and ${participants[1].name}'; - } else if (length == 3) { - return '${participants[0].name}, ${participants[1].name} and ${participants[2].name}'; - } else { - final remaining = length - 2; - return '${participants[0].name}, ${participants[1].name} and +$remaining more'; - } - } -} diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/participant_avatars.dart b/packages/stream_video_flutter/lib/src/call_screen/common/participant_avatars.dart deleted file mode 100644 index 4c63b227d..000000000 --- a/packages/stream_video_flutter/lib/src/call_screen/common/participant_avatars.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../stream_video_flutter.dart'; - -/// Renders avatars of the participants on the outgoing call and incoming -/// call screens. -class ParticipantAvatars extends StatelessWidget { - /// Creates a new instance of [ParticipantAvatars]. - const ParticipantAvatars({ - super.key, - required this.participants, - this.singleParticipantAvatarTheme = const StreamUserAvatarThemeData( - initialsTextStyle: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - constraints: BoxConstraints( - minHeight: 160, - minWidth: 160, - ), - borderRadius: BorderRadius.all( - Radius.circular(80), - ), - ), - this.multipleParticipantAvatarTheme = const StreamUserAvatarThemeData( - initialsTextStyle: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - constraints: BoxConstraints( - minHeight: 80, - minWidth: 80, - ), - borderRadius: BorderRadius.all( - Radius.circular(40), - ), - ), - }); - - /// The list of participants to display. - final List participants; - - /// Theme for the avatar in a call with one participant. - final StreamUserAvatarThemeData singleParticipantAvatarTheme; - - /// Theme for the avatar in a call with multiple participants. - final StreamUserAvatarThemeData multipleParticipantAvatarTheme; - - @override - Widget build(BuildContext context) { - final streamChatTheme = StreamVideoTheme.of(context); - final length = participants.length; - - if (length == 1) { - return StreamUserAvatarTheme( - data: singleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[0], - ), - ); - } else if (length == 2) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamUserAvatarTheme( - data: multipleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[0], - ), - ), - const SizedBox( - width: 32, - ), - StreamUserAvatarTheme( - data: multipleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[1], - ), - ), - ], - ); - } else if (length >= 3) { - return Column( - children: [ - StreamUserAvatarTheme( - data: multipleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[0], - ), - ), - const SizedBox( - height: 8, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamUserAvatarTheme( - data: multipleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[1], - ), - ), - const SizedBox( - width: 32, - ), - if (length > 3) - Container( - constraints: multipleParticipantAvatarTheme.constraints, - child: DecoratedBox( - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentPrimary, - borderRadius: multipleParticipantAvatarTheme.borderRadius, - ), - child: Center( - child: Text( - '+${length - 2}', - style: multipleParticipantAvatarTheme.initialsTextStyle, - ), - ), - ), - ) - else - StreamUserAvatarTheme( - data: multipleParticipantAvatarTheme, - child: StreamUserAvatar( - user: participants[2], - ), - ), - ], - ), - ], - ); - } else { - return const SizedBox(); - } - } -} diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart new file mode 100644 index 000000000..c58b626ec --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart @@ -0,0 +1,60 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../../../stream_video_flutter.dart'; + +/// What an outgoing ringing screen is drawn on: the caller's own camera, +/// blurred and washed out so the call details stay legible on top of it. +/// +/// Falls back to a plain fill whenever there is no camera to draw — it is off, +/// it has not opened yet, or it refused to open. That is the design's +/// camera-off state, not an error state. +class RingingCallBackground extends StatelessWidget { + /// Creates a new instance of [RingingCallBackground]. + const RingingCallBackground({ + super.key, + required this.style, + this.cameraTrack, + required this.child, + }); + + /// The resolved style of the screen. + final StreamRingingCallStyle style; + + /// The camera to draw, or null to draw the fill alone. + final RtcLocalCameraTrack? cameraTrack; + + /// The screen drawn on top. + final Widget child; + + @override + Widget build(BuildContext context) { + final track = cameraTrack; + + return Stack( + fit: StackFit.expand, + children: [ + ColoredBox(color: style.backgroundColor!), + if (track != null) + VideoTrackRenderer( + videoTrack: track, + mirror: track.mediaConstraints.facingMode == FacingMode.user, + ), + // Over the fill as well as over the camera: the scrim is what the + // details' contrast is designed against, and dropping it when the + // camera is off would leave them on a different background. + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: style.blurSigma!, + sigmaY: style.blurSigma!, + ), + child: ColoredBox(color: style.scrimColor!), + ), + ), + child, + ], + ); + } +} diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart new file mode 100644 index 000000000..30af134fc --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import '../../../stream_video_flutter.dart'; +import '../../l10n/localization_extension.dart'; + +/// Who is ringing and what the call is doing: an avatar over a name and a +/// status line. +/// +/// Shared by the incoming and outgoing screens, which differ in what sits +/// behind it and in the colors [style] resolves to, not in the block itself. +class RingingCallDetails extends StatelessWidget { + /// Creates a new instance of [RingingCallDetails]. + const RingingCallDetails({ + super.key, + required this.participants, + required this.status, + required this.style, + this.avatar, + this.nameLine, + }); + + /// The people the call is ringing. + final List participants; + + /// The line under the name — what the call is doing right now. + final String status; + + /// The resolved style of the screen this block sits on. + final StreamRingingCallStyle style; + + /// Drawn in place of the avatar, when the host supplied one. + final Widget? avatar; + + /// Drawn in place of the name, when the host supplied one. + final Widget? nameLine; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + spacing: style.contentSpacing!, + children: [ + avatar ?? _buildAvatar(), + Column( + mainAxisSize: MainAxisSize.min, + spacing: style.titleSpacing!, + children: [ + nameLine ?? + Text( + nameLineFor(context, participants), + style: style.titleTextStyle, + textAlign: TextAlign.center, + ), + Text(status, style: style.statusTextStyle, textAlign: .center), + ], + ), + ], + ); + } + + Widget _buildAvatar() { + if (participants.length == 1) { + return StreamAvatarTheme( + data: style.avatarTheme!, + child: StreamUserAvatar(user: participants.first), + ); + } + + // The group sizes and borders its own children, so they are handed over + // bare rather than under the single avatar's theme. + return StreamAvatarGroup( + size: style.avatarGroupSize, + children: [ + for (final participant in participants) + StreamUserAvatar(user: participant), + ], + ); + } + + /// The one line naming everyone [participants] is ringing. + /// + /// Past two people it names two and counts the rest: the block is centred on + /// one line, and a third name pushes it past the screen on all but the + /// shortest of names. + static String nameLineFor( + BuildContext context, + List participants, + ) { + final translations = context.translations; + final names = participants.map((it) => it.name).toList(); + + return switch (names.length) { + 0 => translations.ringingNobody, + 1 => names.first, + 2 => translations.ringingTwoCallers(names[0], names[1]), + _ => translations.ringingManyCallers( + names[0], + names[1], + names.length - 2, + ), + }; + } +} diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart new file mode 100644 index 000000000..dab7d278c --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart @@ -0,0 +1,61 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../stream_video_flutter.dart'; + +/// The values a ringing screen falls back to when neither its theme nor its +/// call site names one. +/// +/// Both screens share a layout and differ in their colors, so the geometry +/// resolves here and each screen overrides the handful of properties that +/// depend on what it is drawn on top of. +/// +/// Never hand an instance of this to a theme: every getter is non-null, so +/// merging one would pin every property of whatever it was merged into. +@internal +abstract class RingingCallStyleDefaults extends StreamRingingCallStyle { + /// Resolves a ringing screen's defaults from the theme on the given context. + RingingCallStyleDefaults(this.context, this.style); + + /// The context the design-system tokens are read from. + final BuildContext context; + + /// The style the theme and the call site resolved to, if any. + final StreamRingingCallStyle? style; + + late final colorScheme = context.streamColorScheme; + late final textTheme = context.streamTextTheme; + late final spacing = context.streamSpacing; + + @override + Color get scrimColor => style?.scrimColor ?? colorScheme.backgroundScrim; + + @override + double get blurSigma => style?.blurSigma ?? 25; + + @override + StreamAvatarThemeData get avatarTheme => const StreamAvatarThemeData( + size: StreamAvatarSize.xxxl, + ).merge(style?.avatarTheme); + + @override + StreamAvatarGroupSize get avatarGroupSize => + style?.avatarGroupSize ?? StreamAvatarGroupSize.xxxl; + + @override + double get contentSpacing => style?.contentSpacing ?? spacing.md; + + @override + double get titleSpacing => style?.titleSpacing ?? spacing.xs; + + @override + double get controlsSpacing => style?.controlsSpacing ?? 80; + + @override + EdgeInsetsGeometry get controlsPadding => + style?.controlsPadding ?? const EdgeInsets.only(bottom: 104); + + @override + double get secondaryControlsSpacing => + style?.secondaryControlsSpacing ?? spacing.xxl; +} diff --git a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart index c0f4be42e..7a5df16f7 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart @@ -1,11 +1,9 @@ -// ignore_for_file: deprecated_member_use_from_same_package - import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; -import '../common/call_background.dart'; -import '../common/calling_participants.dart'; -import '../common/participant_avatars.dart'; +import '../../l10n/localization_extension.dart'; +import '../common/ringing_call_details.dart'; +import '../common/ringing_call_style_defaults.dart'; import 'incoming_call_controls.dart'; /// Represents the Incoming Call state and UI, when the user is called by @@ -18,11 +16,7 @@ class StreamIncomingCallContent extends StatefulWidget { this.onDeclineCallTap, this.onMicrophoneTap, this.onCameraTap, - this.singleParticipantAvatarTheme, - this.multipleParticipantAvatarTheme, - this.singleParticipantTextStyle, - this.multipleParticipantTextStyle, - this.callingLabelTextStyle, + this.style, this.participantsAvatarWidgetBuilder, this.participantsDisplayNameWidgetBuilder, }); @@ -42,20 +36,11 @@ class StreamIncomingCallContent extends StatefulWidget { /// The action to perform when the camera button is tapped. final VoidCallback? onCameraTap; - /// Theme for the avatar in a call with one participant. - final StreamUserAvatarThemeData? singleParticipantAvatarTheme; - - /// Theme for the avatar in a call with multiple participants. - final StreamUserAvatarThemeData? multipleParticipantAvatarTheme; - - /// Text style for the participant label in a call with one participant. - final TextStyle? singleParticipantTextStyle; - - /// Text style for the participant label in a call with multiple participants. - final TextStyle? multipleParticipantTextStyle; - - /// Text style for the calling label. - final TextStyle? callingLabelTextStyle; + /// Overrides for this screen alone. + /// + /// Resolved over [StreamIncomingCallTheme], so setting one property here + /// leaves the rest coming from the theme. + final StreamRingingCallStyle? style; /// Builder used to create a custom widget for participants avatars. final CallWidgetBuilderWithData? @@ -75,72 +60,53 @@ class _StreamIncomingCallContentState extends State { @override Widget build(BuildContext context) { - final theme = StreamIncomingOutgoingCallTheme.incomingCallThemeOf(context); - - final singleParticipantAvatarTheme = - widget.singleParticipantAvatarTheme ?? - theme.singleParticipantAvatarTheme; - final multipleParticipantAvatarTheme = - widget.multipleParticipantAvatarTheme ?? - theme.multipleParticipantAvatarTheme; - final singleParticipantTextStyle = - widget.singleParticipantTextStyle ?? theme.singleParticipantTextStyle; - final multipleParticipantTextStyle = - widget.multipleParticipantTextStyle ?? - theme.multipleParticipantTextStyle; - final callingLabelTextStyle = - widget.callingLabelTextStyle ?? theme.callingLabelTextStyle; - - Widget buildContent(List users) => CallBackground( - participants: users, + final style = _StreamIncomingCallStyleDefaults( + context, + StreamIncomingCallTheme.of(context).style?.merge(widget.style) ?? + widget.style, + ); + + Widget buildContent(List users) => ColoredBox( + color: style.backgroundColor, child: Material( color: Colors.transparent, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - widget.participantsAvatarWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: users), - ) ?? - ParticipantAvatars( + child: SafeArea( + child: Stack( + children: [ + Center( + child: RingingCallDetails( participants: users, - singleParticipantAvatarTheme: singleParticipantAvatarTheme, - multipleParticipantAvatarTheme: - multipleParticipantAvatarTheme, - ), - widget.participantsDisplayNameWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: users), - ) ?? - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 64, - vertical: 32, + status: context.translations.ringingIncomingCall, + style: style, + avatar: widget.participantsAvatarWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: users), + ), + nameLine: widget.participantsDisplayNameWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: users), ), - child: CallingParticipants( - participants: users, - singleParticipantTextStyle: singleParticipantTextStyle, - multipleParticipantTextStyle: multipleParticipantTextStyle, + ), + ), + Align( + alignment: AlignmentDirectional.bottomCenter, + child: Padding( + padding: style.controlsPadding, + child: IncomingCallControls( + style: style, + isMicrophoneEnabled: connectOptions.microphone.isEnabled, + isCameraEnabled: connectOptions.camera.isEnabled, + onAcceptCallTap: _onAcceptCallTap, + onDeclineCallTap: () => _onDeclineCallTap(context), + onMicrophoneTap: () => _onMicrophoneTap(context), + onCameraTap: () => _onCameraTap(context), ), ), - Text( - // TODO hardcoded text - 'Incoming Call...', - style: callingLabelTextStyle, - ), - const Spacer(), - IncomingCallControls( - isMicrophoneEnabled: connectOptions.microphone.isEnabled, - isCameraEnabled: connectOptions.camera.isEnabled, - onAcceptCallTap: _onAcceptCallTap, - onDeclineCallTap: () => _onDeclineCallTap(context), - onMicrophoneTap: () => _onMicrophoneTap(context), - onCameraTap: () => _onCameraTap(context), - ), - ], + ), + ], + ), ), ), ); @@ -191,3 +157,25 @@ class _StreamIncomingCallContentState extends State { } } } + +// Default style values for [StreamIncomingCallContent]. +// +// The screen sits on a surface of its own, so its text is the ordinary text +// of the app rather than text drawn on top of something. +class _StreamIncomingCallStyleDefaults extends RingingCallStyleDefaults { + _StreamIncomingCallStyleDefaults(super.context, super.style); + + @override + Color get backgroundColor => + style?.backgroundColor ?? colorScheme.backgroundApp; + + @override + TextStyle get titleTextStyle => + style?.titleTextStyle ?? + textTheme.headingLg.copyWith(color: colorScheme.textPrimary); + + @override + TextStyle get statusTextStyle => + style?.statusTextStyle ?? + textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary); +} diff --git a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart index 992162abe..38b4f6f98 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; +import '../../l10n/localization_extension.dart'; -/// Represents a set of controls the user can use on the calling screen -/// to accept/cancel the call, toggle their audio and video state. +/// The controls of the incoming ringing screen: answer and decline, over the +/// microphone and camera the call will be joined with. class IncomingCallControls extends StatelessWidget { /// Creates a new instance of [IncomingCallControls]. const IncomingCallControls({ super.key, + required this.style, this.isMicrophoneEnabled = false, this.isCameraEnabled = false, required this.onAcceptCallTap, @@ -16,6 +18,9 @@ class IncomingCallControls extends StatelessWidget { required this.onCameraTap, }); + /// The resolved style of the screen these controls sit on. + final StreamRingingCallStyle style; + /// If camera is enabled. final bool isCameraEnabled; @@ -37,50 +42,54 @@ class IncomingCallControls extends StatelessWidget { @override Widget build(BuildContext context) { final icons = context.streamIcons; + final translations = context.translations; - return Padding( - padding: const EdgeInsets.only(bottom: 64), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CallControlButton( - icon: Icon(icons.phoneDownFill), - tone: .negative, - onPressed: onDeclineCallTap, - ), - CallControlButton( - icon: Icon(icons.phoneFill), - tone: .positive, - onPressed: onAcceptCallTap, - ), - ], - ), - const SizedBox( - height: 32, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CallControlButton( - icon: Icon( - isMicrophoneEnabled ? icons.voiceFill : icons.voiceOffFill, - ), - tone: isMicrophoneEnabled ? .neutral : .negative, - onPressed: onMicrophoneTap, + return Column( + mainAxisSize: MainAxisSize.min, + spacing: style.secondaryControlsSpacing!, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: style.controlsSpacing!, + children: [ + CallRingingButton( + icon: Icon(icons.phoneDownFill), + tone: .negative, + label: translations.ringingDecline, + onPressed: onDeclineCallTap, + ), + CallRingingButton( + icon: Icon(icons.phoneFill), + tone: .positive, + label: translations.ringingAccept, + onPressed: onAcceptCallTap, + ), + ], + ), + // The call is joined with whatever these say, so they stay on a screen + // the design draws without them. + Row( + mainAxisSize: MainAxisSize.min, + spacing: context.streamSpacing.sm, + children: [ + CallControlButton( + icon: Icon( + isMicrophoneEnabled ? icons.voiceFill : icons.voiceOffFill, ), - CallControlButton( - icon: Icon( - isCameraEnabled ? icons.videoFill : icons.videoOffFill, - ), - tone: isCameraEnabled ? .neutral : .negative, - onPressed: onCameraTap, + tone: isMicrophoneEnabled ? .neutral : .negative, + onPressed: onMicrophoneTap, + ), + CallControlButton( + icon: Icon( + isCameraEnabled ? icons.videoFill : icons.videoOffFill, ), - ], - ), - ], - ), + tone: isCameraEnabled ? .neutral : .negative, + onPressed: onCameraTap, + ), + ], + ), + ], ); } } diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart index 8ceba914c..4761a2558 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart @@ -1,21 +1,13 @@ -// ignore_for_file: deprecated_member_use_from_same_package +import 'dart:async'; import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; -import '../common/call_background.dart'; -import '../common/calling_participants.dart'; -import '../common/participant_avatars.dart'; +import '../../l10n/localization_extension.dart'; +import '../common/ringing_call_details.dart'; +import '../common/ringing_call_style_defaults.dart'; import 'outgoing_call_controls.dart'; -typedef OutgoingCallBackground = - Widget Function( - Call call, - CallState callState, - List participants, - Widget child, - ); - /// Represents the Outgoing Call state and UI, when the user is calling /// other people. class StreamOutgoingCallContent extends StatefulWidget { @@ -26,11 +18,8 @@ class StreamOutgoingCallContent extends StatefulWidget { this.onCancelCallTap, this.onMicrophoneTap, this.onCameraTap, - this.singleParticipantAvatarTheme, - this.multipleParticipantAvatarTheme, - this.singleParticipantTextStyle, - this.multipleParticipantTextStyle, - this.callingLabelTextStyle, + this.style, + this.controller, this.callBackgroundWidgetBuilder, this.participantsAvatarWidgetBuilder, this.participantsDisplayNameWidgetBuilder, @@ -48,20 +37,18 @@ class StreamOutgoingCallContent extends StatefulWidget { /// The action to perform when the camera button is tapped. final VoidCallback? onCameraTap; - /// Theme for the avatar in a call with one participant. - final StreamUserAvatarThemeData? singleParticipantAvatarTheme; - - /// Theme for the avatar in a call with multiple participants. - final StreamUserAvatarThemeData? multipleParticipantAvatarTheme; - - /// Text style for the participant label in a call with one participant. - final TextStyle? singleParticipantTextStyle; - - /// Text style for the participant label in a call with multiple participants. - final TextStyle? multipleParticipantTextStyle; + /// Overrides for this screen alone. + /// + /// Resolved over [StreamOutgoingCallTheme], so setting one property here + /// leaves the rest coming from the theme. + final StreamRingingCallStyle? style; - /// Text style for the calling label. - final TextStyle? callingLabelTextStyle; + /// The camera the screen previews and the call is placed with. + /// + /// When null the screen makes one for [call] and disposes of it itself. + /// Supply one to keep the preview running across a screen the host rebuilds, + /// or to drive the camera from outside this widget. + final StreamRingingCameraController? controller; /// Builder used to create a custom widget for participants avatars. final CallWidgetBuilderWithData? @@ -71,9 +58,12 @@ class StreamOutgoingCallContent extends StatefulWidget { final CallWidgetBuilderWithData? participantsDisplayNameWidgetBuilder; - /// A widget that is placed behind the outgoing call UI instead of the Stream default + /// A widget that is placed behind the outgoing call UI instead of the Stream + /// default. /// - /// Preferably use a [Stack] widget to layer your UI like in the default [CallBackground]. + /// The default draws the caller's own camera behind a blur and a scrim. + /// Preferably use a [Stack] widget to layer your UI like in the default + /// [RingingCallBackground]. final CallWidgetChildBuilder? callBackgroundWidgetBuilder; @override @@ -82,73 +72,95 @@ class StreamOutgoingCallContent extends StatefulWidget { } class _StreamOutgoingCallContentState extends State { - CallConnectOptions get connectOptions => widget.call.connectOptions; + StreamRingingCameraController? _ownedController; + + StreamRingingCameraController get _controller => + widget.controller ?? _ownedController!; + + @override + void initState() { + super.initState(); + _createOwnedController(); + } + + @override + void didUpdateWidget(StreamOutgoingCallContent oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller || + widget.call != oldWidget.call) { + _disposeOwnedController(); + _createOwnedController(); + } + } + + @override + void dispose() { + _disposeOwnedController(); + super.dispose(); + } + + void _createOwnedController() { + if (widget.controller != null) return; + _ownedController = StreamRingingCameraController(call: widget.call) + ..addListener(_onControllerChanged); + } + + void _disposeOwnedController() { + _ownedController + ?..removeListener(_onControllerChanged) + ..dispose(); + _ownedController = null; + } + + void _onControllerChanged() => setState(() {}); @override Widget build(BuildContext context) { - final theme = StreamIncomingOutgoingCallTheme.outgoingCallThemeOf(context); - - final singleParticipantAvatarTheme = - widget.singleParticipantAvatarTheme ?? - theme.singleParticipantAvatarTheme; - final multipleParticipantAvatarTheme = - widget.multipleParticipantAvatarTheme ?? - theme.multipleParticipantAvatarTheme; - final singleParticipantTextStyle = - widget.singleParticipantTextStyle ?? theme.singleParticipantTextStyle; - final multipleParticipantTextStyle = - widget.multipleParticipantTextStyle ?? - theme.multipleParticipantTextStyle; - final callingLabelTextStyle = - widget.callingLabelTextStyle ?? theme.callingLabelTextStyle; + final style = _StreamOutgoingCallStyleDefaults( + context, + StreamOutgoingCallTheme.of(context).style?.merge(widget.style) ?? + widget.style, + ); Widget buildContent(List participants) { final child = Material( color: Colors.transparent, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - widget.participantsAvatarWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: participants), - ) ?? - ParticipantAvatars( + child: SafeArea( + child: Stack( + children: [ + Center( + child: RingingCallDetails( participants: participants, - singleParticipantAvatarTheme: singleParticipantAvatarTheme, - multipleParticipantAvatarTheme: - multipleParticipantAvatarTheme, - ), - widget.participantsDisplayNameWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: participants), - ) ?? - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 64, - vertical: 32, + status: context.translations.ringingCalling, + style: style, + avatar: widget.participantsAvatarWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: participants), + ), + nameLine: widget.participantsDisplayNameWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: participants), ), - child: CallingParticipants( - participants: participants, - singleParticipantTextStyle: singleParticipantTextStyle, - multipleParticipantTextStyle: multipleParticipantTextStyle, + ), + ), + Align( + alignment: AlignmentDirectional.bottomCenter, + child: Padding( + padding: style.controlsPadding, + child: OutgoingCallControls( + style: style, + isMicrophoneEnabled: _controller.microphoneEnabled, + isCameraEnabled: _controller.cameraEnabled, + onCancelCallTap: () => _onCancelCallTap(context), + onMicrophoneTap: _onMicrophoneTap, + onCameraTap: _onCameraTap, ), ), - Text( - 'Calling…', - style: callingLabelTextStyle, - ), - const Spacer(), - OutgoingCallControls( - isMicrophoneEnabled: connectOptions.microphone.isEnabled, - isCameraEnabled: connectOptions.camera.isEnabled, - onCancelCallTap: () => _onCancelCallTap(context), - onMicrophoneTap: () => _onMicrophoneTap(context), - onCameraTap: () => _onCameraTap(context), - ), - ], + ), + ], + ), ), ); @@ -157,8 +169,9 @@ class _StreamOutgoingCallContentState extends State { widget.call, child, ) ?? - CallBackground( - participants: participants, + RingingCallBackground( + style: style, + cameraTrack: _controller.cameraTrack, child: child, ); } @@ -179,25 +192,41 @@ class _StreamOutgoingCallContentState extends State { } } - Future _onMicrophoneTap(BuildContext context) async { + void _onMicrophoneTap() { if (widget.onMicrophoneTap != null) { widget.onMicrophoneTap!(); } else { - widget.call.connectOptions = connectOptions.copyWith( - microphone: connectOptions.microphone.toggle(), - ); - return setState(() => {}); + _controller.toggleMicrophone(); } } - Future _onCameraTap(BuildContext context) async { + void _onCameraTap() { if (widget.onCameraTap != null) { widget.onCameraTap!(); } else { - widget.call.connectOptions = connectOptions.copyWith( - camera: connectOptions.camera.toggle(), - ); - return setState(() => {}); + unawaited(_controller.toggleCamera()); } } } + +// Default style values for [StreamOutgoingCallContent]. +// +// The screen is drawn on top of the caller's own camera, so its text is the +// text used on an image rather than on a surface. +class _StreamOutgoingCallStyleDefaults extends RingingCallStyleDefaults { + _StreamOutgoingCallStyleDefaults(super.context, super.style); + + @override + Color get backgroundColor => + style?.backgroundColor ?? colorScheme.backgroundApp; + + @override + TextStyle get titleTextStyle => + style?.titleTextStyle ?? + textTheme.headingLg.copyWith(color: colorScheme.textOnAccent); + + @override + TextStyle get statusTextStyle => + style?.statusTextStyle ?? + textTheme.bodyDefault.copyWith(color: colorScheme.textOnAccent); +} diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart index 6c588392d..1459c1d61 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; -/// Represents a set of controls the user can use on the calling screen -/// to cancel the call, toggle their audio and video state. +/// The controls of the outgoing ringing screen: the microphone and camera the +/// call will be placed with, over the button that cancels it. class OutgoingCallControls extends StatelessWidget { /// Creates a new instance of [OutgoingCallControls]. const OutgoingCallControls({ super.key, + required this.style, this.isMicrophoneEnabled = false, this.isCameraEnabled = false, required this.onCancelCallTap, @@ -15,13 +16,16 @@ class OutgoingCallControls extends StatelessWidget { required this.onCameraTap, }); + /// The resolved style of the screen these controls sit on. + final StreamRingingCallStyle style; + /// If camera is enabled. final bool isCameraEnabled; /// If microphone is enabled. final bool isMicrophoneEnabled; - /// The action to perform when the hang up button is tapped. + /// The action to perform when the cancel call button is tapped. final VoidCallback onCancelCallTap; /// The action to perform when the microphone button is tapped. @@ -34,39 +38,36 @@ class OutgoingCallControls extends StatelessWidget { Widget build(BuildContext context) { final icons = context.streamIcons; - return Padding( - padding: const EdgeInsets.only(bottom: 64), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CallControlButton( - icon: Icon( - isMicrophoneEnabled ? icons.voiceFill : icons.voiceOffFill, - ), - tone: isMicrophoneEnabled ? .neutral : .negative, - onPressed: onMicrophoneTap, + return Column( + mainAxisSize: MainAxisSize.min, + spacing: style.secondaryControlsSpacing!, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + spacing: style.controlsSpacing!, + children: [ + CallControlButton( + icon: Icon( + isMicrophoneEnabled ? icons.voiceFill : icons.voiceOffFill, ), - CallControlButton( - icon: Icon( - isCameraEnabled ? icons.videoFill : icons.videoOffFill, - ), - tone: isCameraEnabled ? .neutral : .negative, - onPressed: onCameraTap, + tone: isMicrophoneEnabled ? .neutral : .negative, + onPressed: onMicrophoneTap, + ), + CallControlButton( + icon: Icon( + isCameraEnabled ? icons.videoFill : icons.videoOffFill, ), - ], - ), - CallControlButton( - icon: Icon(icons.phoneDownFill), - tone: .negative, - onPressed: onCancelCallTap, - ), - const SizedBox( - height: 32, - ), - ], - ), + tone: isCameraEnabled ? .neutral : .negative, + onPressed: onCameraTap, + ), + ], + ), + CallRingingButton( + icon: Icon(icons.phoneDownFill), + tone: .negative, + onPressed: onCancelCallTap, + ), + ], ); } } diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/ringing_camera_controller.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/ringing_camera_controller.dart new file mode 100644 index 000000000..1b01dadcf --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/ringing_camera_controller.dart @@ -0,0 +1,173 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../../../stream_video_flutter.dart'; + +/// Opens the camera the outgoing screen previews. See +/// [StreamRingingCameraController]. +/// +/// Injectable because [RtcLocalTrack]'s factories are static, so a test has no +/// other way to produce a track. +@visibleForTesting +typedef RingingCameraTrackOpener = Future Function(); + +/// The microphone and camera an outgoing call will be placed with, and the +/// camera track the ringing screen shows behind its blur. +/// +/// The track is written into [Call.connectOptions] as [TrackOption.provided] +/// the moment it opens, so the call carries on with the camera the caller was +/// already previewing rather than opening a second one when the callee picks +/// up. +class StreamRingingCameraController extends ChangeNotifier { + /// Creates a controller previewing the camera [call] will be placed with. + StreamRingingCameraController({ + required this.call, + @visibleForTesting RingingCameraTrackOpener? openCameraTrack, + }) : _openCameraTrack = openCameraTrack { + // The call was created with a camera preference; the preview starts in + // whatever state that named. + if (call.connectOptions.camera.isEnabled) { + unawaited(setCameraEnabled(enabled: true)); + } + } + + late final _logger = taggedLogger(tag: 'SV:RingingCamera'); + + /// The call being placed. + final Call call; + + final RingingCameraTrackOpener? _openCameraTrack; + + RtcLocalCameraTrack? _cameraTrack; + StreamDeviceError? _cameraError; + bool _opening = false; + bool _cameraDesired = false; + bool _disposed = false; + + /// The running camera, or null while it is off or still opening. + RtcLocalCameraTrack? get cameraTrack => _cameraTrack; + + /// Why the camera could not be opened, if it could not be. + /// + /// Ringing carries on regardless: a camera that will not start is not a + /// reason to stop the call from being placed. + StreamDeviceError? get cameraError => _cameraError; + + /// Whether the camera is running. + /// + /// Not [CallConnectOptions.camera]'s own `isEnabled`, which reads false for + /// a provided track: once the preview is running the option holds the track + /// rather than a request to open one. + bool get cameraEnabled => _cameraTrack != null; + + /// Whether the call will be placed with the microphone open. + bool get microphoneEnabled => call.connectOptions.microphone.isEnabled; + + /// Turns the camera on if it is off, and off if it is on. + Future toggleCamera() => setCameraEnabled(enabled: !cameraEnabled); + + /// Opens the camera, or closes it. + Future setCameraEnabled({required bool enabled}) async { + _cameraDesired = enabled; + + if (!enabled) { + final track = _cameraTrack; + _cameraTrack = null; + call.connectOptions = call.connectOptions.copyWith( + camera: TrackOption.disabled(), + ); + _notify(); + await track?.stop(); + return; + } + + if (_cameraTrack != null || _opening) return; + await _openCamera(); + } + + /// Turns the microphone on if it is off, and off if it is on. + void toggleMicrophone() => setMicrophoneEnabled(enabled: !microphoneEnabled); + + /// Records whether the call should be placed with the microphone open. + /// + /// Nothing is opened here — there is no preview to feed, and the call opens + /// the microphone itself when it connects. + void setMicrophoneEnabled({required bool enabled}) { + call.connectOptions = call.connectOptions.copyWith( + microphone: TrackOption.fromSetting(enabled: enabled), + ); + _notify(); + } + + Future _openCamera() async { + _opening = true; + _notify(); + + try { + final track = await _open(); + + // The user can turn the camera off, or leave, while the platform is + // still opening it. The track that lands then has no owner. + if (_disposed || !_cameraDesired) { + _opening = false; + await track.stop(); + return; + } + + _cameraTrack = track; + _cameraError = null; + call.connectOptions = call.connectOptions.copyWith( + camera: TrackOption.provided(track), + ); + } catch (e, stk) { + _logger.e(() => 'Error creating camera track: $e\n$stk'); + _cameraError = StreamDeviceError.from(e, stk); + // The call is still placed, with the camera off rather than pending. + call.connectOptions = call.connectOptions.copyWith( + camera: TrackOption.disabled(), + ); + } + + _opening = false; + _notify(); + } + + Future _open() async { + if (_openCameraTrack case final open?) return open(); + return RtcLocalTrack.camera( + nativeFactory: await call.ensureNativeFactory(), + ); + } + + void _notify() { + if (_disposed) return; + notifyListeners(); + } + + @override + void dispose() { + // Set first: an open already in flight reads this to decide whether the + // track it is about to produce has an owner. + _disposed = true; + + // A call that is going ahead owns the track now — it was handed over as + // TrackOption.provided, and stopping it here would cut the video the + // caller just joined with. A call that was cancelled or refused leaves + // nobody to turn the camera off but this. + if (!_callIsGoingAhead) { + _cameraTrack?.stop().onError((e, stk) { + _logger.e(() => 'Error stopping the camera track: $e\n$stk'); + }); + } + + _cameraTrack = null; + super.dispose(); + } + + bool get _callIsGoingAhead => switch (call.state.value.status) { + CallStatusOutgoing(:final acceptedByCallee) => acceptedByCallee, + CallStatusIdle() || CallStatusDisconnected() => false, + _ => true, + }; +} diff --git a/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_en.arb b/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_en.arb index 7c50d07e9..888e3112d 100644 --- a/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_en.arb +++ b/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_en.arb @@ -230,5 +230,42 @@ "type": "int" } } + }, + "ringingIncomingCall": "Incoming call", + "@ringingIncomingCall": { + "description": "Status line on the incoming ringing screen, under the caller's name" + }, + "ringingCalling": "Calling\u2026", + "@ringingCalling": { + "description": "Status line on the outgoing ringing screen, while the other side is being rung" + }, + "ringingAccept": "Accept", + "@ringingAccept": { + "description": "Label under the button that answers an incoming call" + }, + "ringingDecline": "Decline", + "@ringingDecline": { + "description": "Label under the button that rejects an incoming call" + }, + "ringingNobody": "No participants", + "@ringingNobody": { + "description": "Shown in place of a name on a ringing screen when the call has no members yet" + }, + "ringingTwoCallers": "{first} and {second}", + "@ringingTwoCallers": { + "description": "The name line on a ringing screen with exactly two members", + "placeholders": { + "first": { "type": "String" }, + "second": { "type": "String" } + } + }, + "ringingManyCallers": "{first}, {second}, and {count, plural, =1{1 other} other{{count} others}}", + "@ringingManyCallers": { + "description": "The name line on a ringing screen with three or more members: two names and a count of the rest", + "placeholders": { + "first": { "type": "String" }, + "second": { "type": "String" }, + "count": { "type": "int" } + } } } diff --git a/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_nl.arb b/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_nl.arb index 83337b948..9bda4ae2a 100644 --- a/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_nl.arb +++ b/packages/stream_video_flutter/lib/src/l10n/arb/stream_video_flutter_nl.arb @@ -51,5 +51,12 @@ "callDurationSpoken": "Gespreksduur {duration}", "callDurationHours": "{count, plural, one{1 uur} other{{count} uur}}", "callDurationMinutes": "{count, plural, one{1 minuut} other{{count} minuten}}", - "callDurationSeconds": "{count, plural, one{1 seconde} other{{count} seconden}}" + "callDurationSeconds": "{count, plural, one{1 seconde} other{{count} seconden}}", + "ringingIncomingCall": "Inkomende oproep", + "ringingCalling": "Bellen\u2026", + "ringingAccept": "Opnemen", + "ringingDecline": "Weigeren", + "ringingNobody": "Geen deelnemers", + "ringingTwoCallers": "{first} en {second}", + "ringingManyCallers": "{first}, {second} en {count, plural, =1{1 ander} other{{count} anderen}}" } diff --git a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations.dart b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations.dart index 3f043ea1e..097038e32 100644 --- a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations.dart +++ b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations.dart @@ -418,6 +418,48 @@ abstract class StreamVideoFlutterLocalizations { /// In en, this message translates to: /// **'{count, plural, one{1 second} other{{count} seconds}}'** String callDurationSeconds(int count); + + /// Status line on the incoming ringing screen, under the caller's name + /// + /// In en, this message translates to: + /// **'Incoming call'** + String get ringingIncomingCall; + + /// Status line on the outgoing ringing screen, while the other side is being rung + /// + /// In en, this message translates to: + /// **'Calling…'** + String get ringingCalling; + + /// Label under the button that answers an incoming call + /// + /// In en, this message translates to: + /// **'Accept'** + String get ringingAccept; + + /// Label under the button that rejects an incoming call + /// + /// In en, this message translates to: + /// **'Decline'** + String get ringingDecline; + + /// Shown in place of a name on a ringing screen when the call has no members yet + /// + /// In en, this message translates to: + /// **'No participants'** + String get ringingNobody; + + /// The name line on a ringing screen with exactly two members + /// + /// In en, this message translates to: + /// **'{first} and {second}'** + String ringingTwoCallers(String first, String second); + + /// The name line on a ringing screen with three or more members: two names and a count of the rest + /// + /// In en, this message translates to: + /// **'{first}, {second}, and {count, plural, =1{1 other} other{{count} others}}'** + String ringingManyCallers(String first, String second, int count); } class _StreamVideoFlutterLocalizationsDelegate diff --git a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_en.dart b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_en.dart index b6315d657..7ccb9dc1c 100644 --- a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_en.dart +++ b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_en.dart @@ -207,4 +207,35 @@ class StreamVideoFlutterLocalizationsEn ); return '$_temp0'; } + + @override + String get ringingIncomingCall => 'Incoming call'; + + @override + String get ringingCalling => 'Calling…'; + + @override + String get ringingAccept => 'Accept'; + + @override + String get ringingDecline => 'Decline'; + + @override + String get ringingNobody => 'No participants'; + + @override + String ringingTwoCallers(String first, String second) { + return '$first and $second'; + } + + @override + String ringingManyCallers(String first, String second, int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count others', + one: '1 other', + ); + return '$first, $second, and $_temp0'; + } } diff --git a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_nl.dart b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_nl.dart index 44987abe6..6e078dc52 100644 --- a/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_nl.dart +++ b/packages/stream_video_flutter/lib/src/l10n/localizations/stream_video_flutter_localizations_nl.dart @@ -209,4 +209,35 @@ class StreamVideoFlutterLocalizationsNl ); return '$_temp0'; } + + @override + String get ringingIncomingCall => 'Inkomende oproep'; + + @override + String get ringingCalling => 'Bellen…'; + + @override + String get ringingAccept => 'Opnemen'; + + @override + String get ringingDecline => 'Weigeren'; + + @override + String get ringingNobody => 'Geen deelnemers'; + + @override + String ringingTwoCallers(String first, String second) { + return '$first en $second'; + } + + @override + String ringingManyCallers(String first, String second, int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count anderen', + one: '1 ander', + ); + return '$first, $second en $_temp0'; + } } diff --git a/packages/stream_video_flutter/lib/src/theme/components/components.dart b/packages/stream_video_flutter/lib/src/theme/components/components.dart index e8922f053..4633d20ad 100644 --- a/packages/stream_video_flutter/lib/src/theme/components/components.dart +++ b/packages/stream_video_flutter/lib/src/theme/components/components.dart @@ -11,3 +11,4 @@ export 'lobby_view_theme.dart'; export 'participant_label_theme.dart'; export 'participant_tile_theme.dart'; export 'picture_in_picture_theme.dart'; +export 'ringing_call_theme.dart'; diff --git a/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.dart b/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.dart new file mode 100644 index 000000000..321de2ae7 --- /dev/null +++ b/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.dart @@ -0,0 +1,250 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../../../stream_video_flutter.dart'; + +part 'ringing_call_theme.g.theme.dart'; + +/// Applies a theme to a descendant [StreamIncomingCallContent]. +/// +/// {@tool snippet} +/// +/// ```dart +/// StreamIncomingCallTheme( +/// data: StreamIncomingCallThemeData( +/// style: StreamRingingCallStyle(statusTextStyle: TextStyle(fontSize: 20)), +/// ), +/// child: child, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamOutgoingCallTheme], the same for the screen that calls out. +/// * [StreamRingingCallStyle], the visual style both carry. +class StreamIncomingCallTheme extends InheritedTheme { + /// Creates an incoming call theme. + const StreamIncomingCallTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The incoming call theme data for descendant widgets. + final StreamIncomingCallThemeData data; + + /// Returns the [StreamIncomingCallThemeData] merged from local and global + /// themes. + /// + /// Merges, so a subtree can override one property and inherit the rest. + static StreamIncomingCallThemeData of(BuildContext context) { + final localTheme = context + .dependOnInheritedWidgetOfExactType(); + return StreamVideoTheme.of( + context, + ).incomingCallContentTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamIncomingCallTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamIncomingCallTheme oldWidget) => + data != oldWidget.data; +} + +/// Applies a theme to a descendant [StreamOutgoingCallContent]. +/// +/// See also: +/// +/// * [StreamIncomingCallTheme], the same for the screen that is called. +/// * [StreamRingingCallStyle], the visual style both carry. +class StreamOutgoingCallTheme extends InheritedTheme { + /// Creates an outgoing call theme. + const StreamOutgoingCallTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The outgoing call theme data for descendant widgets. + final StreamOutgoingCallThemeData data; + + /// Returns the [StreamOutgoingCallThemeData] merged from local and global + /// themes. + /// + /// Merges, so a subtree can override one property and inherit the rest. + static StreamOutgoingCallThemeData of(BuildContext context) { + final localTheme = context + .dependOnInheritedWidgetOfExactType(); + return StreamVideoTheme.of( + context, + ).outgoingCallContentTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamOutgoingCallTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamOutgoingCallTheme oldWidget) => + data != oldWidget.data; +} + +/// Theme data for customizing [StreamIncomingCallContent]. +/// +/// See also: +/// +/// * [StreamRingingCallStyle], the style embedded here. +/// * [StreamIncomingCallTheme], for overriding it in a subtree. +@themeGen +@immutable +class StreamIncomingCallThemeData with _$StreamIncomingCallThemeData { + /// Creates incoming call theme data. + const StreamIncomingCallThemeData({this.style}); + + /// Visual styling for the screen. + final StreamRingingCallStyle? style; + + /// Linearly interpolate between two theme data objects. + static StreamIncomingCallThemeData? lerp( + StreamIncomingCallThemeData? a, + StreamIncomingCallThemeData? b, + double t, + ) => _$StreamIncomingCallThemeData.lerp(a, b, t); +} + +/// Theme data for customizing [StreamOutgoingCallContent]. +/// +/// Separate from [StreamIncomingCallThemeData] so the two screens can be +/// themed apart: they share a style shape but not a background, and the +/// outgoing one draws its text on top of the camera rather than on a surface. +/// +/// See also: +/// +/// * [StreamRingingCallStyle], the style embedded here. +/// * [StreamOutgoingCallTheme], for overriding it in a subtree. +@themeGen +@immutable +class StreamOutgoingCallThemeData with _$StreamOutgoingCallThemeData { + /// Creates outgoing call theme data. + const StreamOutgoingCallThemeData({this.style}); + + /// Visual styling for the screen. + final StreamRingingCallStyle? style; + + /// Linearly interpolate between two theme data objects. + static StreamOutgoingCallThemeData? lerp( + StreamOutgoingCallThemeData? a, + StreamOutgoingCallThemeData? b, + double t, + ) => _$StreamOutgoingCallThemeData.lerp(a, b, t); +} + +/// Visual styling properties for a ringing call screen. +/// +/// Both ringing screens are the same layout — an avatar over a name and a +/// status line, with the call controls below — so they share one style shape. +/// What differs is where it resolves its defaults from: the incoming screen +/// sits on a surface, the outgoing one on the local camera. +@themeGen +@immutable +class StreamRingingCallStyle with _$StreamRingingCallStyle { + /// Creates a ringing call style with optional property overrides. + const StreamRingingCallStyle({ + this.backgroundColor, + this.scrimColor, + this.blurSigma, + this.avatarTheme, + this.avatarGroupSize, + this.contentSpacing, + this.titleSpacing, + this.titleTextStyle, + this.statusTextStyle, + this.controlsSpacing, + this.controlsPadding, + this.secondaryControlsSpacing, + }); + + /// The fill behind the whole screen. + /// + /// Defaults to `colorScheme.backgroundApp` on the incoming screen. The + /// outgoing screen draws the local camera instead and only falls back to + /// this when there is no camera to draw. + final Color? backgroundColor; + + /// The wash drawn over the camera on the outgoing screen. + /// + /// Defaults to `colorScheme.backgroundScrim`. Unused by the incoming screen, + /// which has nothing to wash over. + final Color? scrimColor; + + /// The blur applied to the camera on the outgoing screen. + /// + /// Defaults to 25. Set to `0` to show the camera sharp; `null` is not the + /// way to switch it off — like every property here it means "no override". + final double? blurSigma; + + /// The theme the single participant's avatar is drawn with. + /// + /// Defaults to `StreamAvatarSize.xxxl`, the 104px size the design gives it. + final StreamAvatarThemeData? avatarTheme; + + /// The size the avatar group is drawn at when more than one person is + /// ringing. + /// + /// Defaults to `StreamAvatarGroupSize.xxxl`, so a group fills the same box + /// as a single avatar. Sized separately because the design system counts + /// group sizes and avatar sizes on two different scales. + final StreamAvatarGroupSize? avatarGroupSize; + + /// The gap between the avatar and the name below it. + /// + /// Defaults to `spacing.md`. + final double? contentSpacing; + + /// The gap between the name and the status line under it. + /// + /// Defaults to `spacing.xs`. + final double? titleSpacing; + + /// The text style of the name of whoever is ringing. + /// + /// Defaults to `textTheme.headingLg`, in `colorScheme.textPrimary` on the + /// incoming screen and `textOnAccent` on the outgoing one. + final TextStyle? titleTextStyle; + + /// The text style of the line saying what the call is doing. + /// + /// Defaults to `textTheme.bodyDefault`, in `colorScheme.textSecondary` on + /// the incoming screen and `textOnAccent` on the outgoing one. + final TextStyle? statusTextStyle; + + /// The gap between the two buttons that answer and decline. + /// + /// Defaults to 80. Wide on purpose: the two do opposite things and are the + /// one pair on the screen that must not be mistaken for each other. + final double? controlsSpacing; + + /// The inset around the call controls. + /// + /// Defaults to 104 from the bottom, above the safe area. + final EdgeInsetsGeometry? controlsPadding; + + /// The gap between the ringing buttons and the microphone and camera + /// toggles under them. + /// + /// Defaults to `spacing.xxl`. + final double? secondaryControlsSpacing; + + /// Linearly interpolate between two styles. + static StreamRingingCallStyle? lerp( + StreamRingingCallStyle? a, + StreamRingingCallStyle? b, + double t, + ) => _$StreamRingingCallStyle.lerp(a, b, t); +} diff --git a/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.g.theme.dart b/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.g.theme.dart new file mode 100644 index 000000000..7f8bf5c63 --- /dev/null +++ b/packages/stream_video_flutter/lib/src/theme/components/ringing_call_theme.g.theme.dart @@ -0,0 +1,306 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'ringing_call_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamIncomingCallThemeData { + bool get canMerge => true; + + static StreamIncomingCallThemeData? lerp( + StreamIncomingCallThemeData? a, + StreamIncomingCallThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamIncomingCallThemeData( + style: StreamRingingCallStyle.lerp(a.style, b.style, t), + ); + } + + StreamIncomingCallThemeData copyWith({StreamRingingCallStyle? style}) { + final _this = (this as StreamIncomingCallThemeData); + + return StreamIncomingCallThemeData(style: style ?? _this.style); + } + + StreamIncomingCallThemeData merge(StreamIncomingCallThemeData? other) { + final _this = (this as StreamIncomingCallThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith(style: _this.style?.merge(other.style) ?? other.style); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamIncomingCallThemeData); + final _other = (other as StreamIncomingCallThemeData); + + return _other.style == _this.style; + } + + @override + int get hashCode { + final _this = (this as StreamIncomingCallThemeData); + + return Object.hash(runtimeType, _this.style); + } +} + +mixin _$StreamOutgoingCallThemeData { + bool get canMerge => true; + + static StreamOutgoingCallThemeData? lerp( + StreamOutgoingCallThemeData? a, + StreamOutgoingCallThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamOutgoingCallThemeData( + style: StreamRingingCallStyle.lerp(a.style, b.style, t), + ); + } + + StreamOutgoingCallThemeData copyWith({StreamRingingCallStyle? style}) { + final _this = (this as StreamOutgoingCallThemeData); + + return StreamOutgoingCallThemeData(style: style ?? _this.style); + } + + StreamOutgoingCallThemeData merge(StreamOutgoingCallThemeData? other) { + final _this = (this as StreamOutgoingCallThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith(style: _this.style?.merge(other.style) ?? other.style); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamOutgoingCallThemeData); + final _other = (other as StreamOutgoingCallThemeData); + + return _other.style == _this.style; + } + + @override + int get hashCode { + final _this = (this as StreamOutgoingCallThemeData); + + return Object.hash(runtimeType, _this.style); + } +} + +mixin _$StreamRingingCallStyle { + bool get canMerge => true; + + static StreamRingingCallStyle? lerp( + StreamRingingCallStyle? a, + StreamRingingCallStyle? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamRingingCallStyle( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + scrimColor: Color.lerp(a.scrimColor, b.scrimColor, t), + blurSigma: lerpDouble$(a.blurSigma, b.blurSigma, t), + avatarTheme: StreamAvatarThemeData.lerp(a.avatarTheme, b.avatarTheme, t), + avatarGroupSize: t < 0.5 ? a.avatarGroupSize : b.avatarGroupSize, + contentSpacing: lerpDouble$(a.contentSpacing, b.contentSpacing, t), + titleSpacing: lerpDouble$(a.titleSpacing, b.titleSpacing, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + statusTextStyle: TextStyle.lerp(a.statusTextStyle, b.statusTextStyle, t), + controlsSpacing: lerpDouble$(a.controlsSpacing, b.controlsSpacing, t), + controlsPadding: EdgeInsetsGeometry.lerp( + a.controlsPadding, + b.controlsPadding, + t, + ), + secondaryControlsSpacing: lerpDouble$( + a.secondaryControlsSpacing, + b.secondaryControlsSpacing, + t, + ), + ); + } + + StreamRingingCallStyle copyWith({ + Color? backgroundColor, + Color? scrimColor, + double? blurSigma, + StreamAvatarThemeData? avatarTheme, + StreamAvatarGroupSize? avatarGroupSize, + double? contentSpacing, + double? titleSpacing, + TextStyle? titleTextStyle, + TextStyle? statusTextStyle, + double? controlsSpacing, + EdgeInsetsGeometry? controlsPadding, + double? secondaryControlsSpacing, + }) { + final _this = (this as StreamRingingCallStyle); + + return StreamRingingCallStyle( + backgroundColor: backgroundColor ?? _this.backgroundColor, + scrimColor: scrimColor ?? _this.scrimColor, + blurSigma: blurSigma ?? _this.blurSigma, + avatarTheme: avatarTheme ?? _this.avatarTheme, + avatarGroupSize: avatarGroupSize ?? _this.avatarGroupSize, + contentSpacing: contentSpacing ?? _this.contentSpacing, + titleSpacing: titleSpacing ?? _this.titleSpacing, + titleTextStyle: titleTextStyle ?? _this.titleTextStyle, + statusTextStyle: statusTextStyle ?? _this.statusTextStyle, + controlsSpacing: controlsSpacing ?? _this.controlsSpacing, + controlsPadding: controlsPadding ?? _this.controlsPadding, + secondaryControlsSpacing: + secondaryControlsSpacing ?? _this.secondaryControlsSpacing, + ); + } + + StreamRingingCallStyle merge(StreamRingingCallStyle? other) { + final _this = (this as StreamRingingCallStyle); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + backgroundColor: other.backgroundColor, + scrimColor: other.scrimColor, + blurSigma: other.blurSigma, + avatarTheme: + _this.avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + avatarGroupSize: other.avatarGroupSize, + contentSpacing: other.contentSpacing, + titleSpacing: other.titleSpacing, + titleTextStyle: + _this.titleTextStyle?.merge(other.titleTextStyle) ?? + other.titleTextStyle, + statusTextStyle: + _this.statusTextStyle?.merge(other.statusTextStyle) ?? + other.statusTextStyle, + controlsSpacing: other.controlsSpacing, + controlsPadding: other.controlsPadding, + secondaryControlsSpacing: other.secondaryControlsSpacing, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamRingingCallStyle); + final _other = (other as StreamRingingCallStyle); + + return _other.backgroundColor == _this.backgroundColor && + _other.scrimColor == _this.scrimColor && + _other.blurSigma == _this.blurSigma && + _other.avatarTheme == _this.avatarTheme && + _other.avatarGroupSize == _this.avatarGroupSize && + _other.contentSpacing == _this.contentSpacing && + _other.titleSpacing == _this.titleSpacing && + _other.titleTextStyle == _this.titleTextStyle && + _other.statusTextStyle == _this.statusTextStyle && + _other.controlsSpacing == _this.controlsSpacing && + _other.controlsPadding == _this.controlsPadding && + _other.secondaryControlsSpacing == _this.secondaryControlsSpacing; + } + + @override + int get hashCode { + final _this = (this as StreamRingingCallStyle); + + return Object.hash( + runtimeType, + _this.backgroundColor, + _this.scrimColor, + _this.blurSigma, + _this.avatarTheme, + _this.avatarGroupSize, + _this.contentSpacing, + _this.titleSpacing, + _this.titleTextStyle, + _this.statusTextStyle, + _this.controlsSpacing, + _this.controlsPadding, + _this.secondaryControlsSpacing, + ); + } +} diff --git a/packages/stream_video_flutter/lib/src/theme/incoming_outgoing_call_theme.dart b/packages/stream_video_flutter/lib/src/theme/incoming_outgoing_call_theme.dart index 7594e69cf..ee8353191 100644 --- a/packages/stream_video_flutter/lib/src/theme/incoming_outgoing_call_theme.dart +++ b/packages/stream_video_flutter/lib/src/theme/incoming_outgoing_call_theme.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -5,9 +7,23 @@ import '../../stream_video_flutter.dart'; /// Defines default property values for [StreamIncomingCallContent] and /// [StreamOutgoingCallContent] widgets. +/// +/// Nothing reads this any more: both ringing screens are built on the design +/// system, and are styled by [StreamIncomingCallThemeData] and +/// [StreamOutgoingCallThemeData], which can differ from each other. +@Deprecated( + 'Use StreamIncomingCallThemeData and StreamOutgoingCallThemeData instead. ' + 'The ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', +) @immutable class StreamIncomingOutgoingCallThemeData with Diagnosticable { /// Creates a new instance of [StreamIncomingOutgoingCallThemeData]. + @Deprecated( + 'Use StreamIncomingCallThemeData and StreamOutgoingCallThemeData instead. ' + 'Will be removed in the next major version.', + ) const StreamIncomingOutgoingCallThemeData({ this.singleParticipantAvatarTheme = const StreamUserAvatarThemeData( initialsTextStyle: TextStyle( @@ -203,8 +219,18 @@ class StreamIncomingOutgoingCallThemeData with Diagnosticable { /// Applies a incoming/outgoing call theme to descendant [StreamIncomingCallContent] /// and [StreamOutgoingCallContent] widgets. +@Deprecated( + 'Use StreamIncomingCallTheme and StreamOutgoingCallTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so wrapping a subtree in one has no effect. ' + 'Will be removed in the next major version.', +) class StreamIncomingOutgoingCallTheme extends InheritedWidget { /// Creates a new instance of [StreamIncomingOutgoingCallTheme]. + @Deprecated( + 'Use StreamIncomingCallTheme and StreamOutgoingCallTheme instead. ' + 'Will be removed in the next major version.', + ) const StreamIncomingOutgoingCallTheme({ super.key, required this.data, diff --git a/packages/stream_video_flutter/lib/src/theme/stream_video_theme.dart b/packages/stream_video_flutter/lib/src/theme/stream_video_theme.dart index 89a7f3a67..940816a33 100644 --- a/packages/stream_video_flutter/lib/src/theme/stream_video_theme.dart +++ b/packages/stream_video_flutter/lib/src/theme/stream_video_theme.dart @@ -37,7 +37,19 @@ class StreamVideoTheme extends ThemeExtension { 'effect. Will be removed in the next major version.', ) StreamLocalVideoThemeData? localVideoTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) StreamIncomingOutgoingCallThemeData? incomingCallTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) StreamIncomingOutgoingCallThemeData? outgoingCallTheme, StreamParticipantTileThemeData? participantTileTheme, StreamFloatingParticipantTileThemeData? floatingParticipantTileTheme, @@ -49,6 +61,8 @@ class StreamVideoTheme extends ThemeExtension { StreamCallButtonBadgeThemeData? callButtonBadgeTheme, StreamCallParticipantsSpotlightThemeData? callParticipantsSpotlightTheme, StreamLivestreamThemeData? livestreamTheme, + StreamIncomingCallThemeData? incomingCallContentTheme, + StreamOutgoingCallThemeData? outgoingCallContentTheme, }) { final isDark = brightness == Brightness.dark; textTheme ??= isDark @@ -100,6 +114,8 @@ class StreamVideoTheme extends ThemeExtension { callButtonBadgeTheme: callButtonBadgeTheme, callParticipantsSpotlightTheme: callParticipantsSpotlightTheme, livestreamTheme: livestreamTheme, + incomingCallContentTheme: incomingCallContentTheme, + outgoingCallContentTheme: outgoingCallContentTheme, ); return defaultTheme.merge(customizedTheme); @@ -134,8 +150,20 @@ class StreamVideoTheme extends ThemeExtension { 'Will be removed in the next major version.', ) required this.localVideoTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) required this.incomingCallTheme, required this.callContentTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) required this.outgoingCallTheme, this.participantTileTheme = const StreamParticipantTileThemeData(), this.floatingParticipantTileTheme = @@ -152,6 +180,8 @@ class StreamVideoTheme extends ThemeExtension { this.callParticipantsSpotlightTheme = const StreamCallParticipantsSpotlightThemeData(), required this.livestreamTheme, + this.incomingCallContentTheme = const StreamIncomingCallThemeData(), + this.outgoingCallContentTheme = const StreamOutgoingCallThemeData(), }); /// Creates a theme from a Material [Theme] @@ -415,13 +445,25 @@ class StreamVideoTheme extends ThemeExtension { ) final StreamLocalVideoThemeData localVideoTheme; - /// Theme for the outgoing call widget. + /// Theme for the incoming call widget. + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) final StreamIncomingOutgoingCallThemeData incomingCallTheme; /// Theme for the call content widget. final StreamCallContentThemeData callContentTheme; /// Theme for the outgoing call widget. + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) final StreamIncomingOutgoingCallThemeData outgoingCallTheme; /// Theme for the participant tile. @@ -456,6 +498,12 @@ class StreamVideoTheme extends ThemeExtension { /// Theme for the outgoing call widget. final StreamLivestreamThemeData livestreamTheme; + /// Theme for the incoming ringing screen. + final StreamIncomingCallThemeData incomingCallContentTheme; + + /// Theme for the outgoing ringing screen. + final StreamOutgoingCallThemeData outgoingCallContentTheme; + /// Creates a copy of [StreamVideoTheme] with specified attributes /// overridden. /// @@ -487,8 +535,20 @@ class StreamVideoTheme extends ThemeExtension { 'reads this. Will be removed in the next major version.', ) StreamLocalVideoThemeData? localVideoTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) StreamIncomingOutgoingCallThemeData? incomingCallTheme, StreamCallContentThemeData? callContentTheme, + @Deprecated( + 'Use incomingCallContentTheme and outgoingCallContentTheme instead. The ' + 'ringing screens are built on the design system now and no longer read ' + 'this, so a theme set here has no effect. ' + 'Will be removed in the next major version.', + ) StreamIncomingOutgoingCallThemeData? outgoingCallTheme, StreamParticipantTileThemeData? participantTileTheme, StreamFloatingParticipantTileThemeData? floatingParticipantTileTheme, @@ -500,6 +560,8 @@ class StreamVideoTheme extends ThemeExtension { StreamCallButtonBadgeThemeData? callButtonBadgeTheme, StreamCallParticipantsSpotlightThemeData? callParticipantsSpotlightTheme, StreamLivestreamThemeData? livestreamTheme, + StreamIncomingCallThemeData? incomingCallContentTheme, + StreamOutgoingCallThemeData? outgoingCallContentTheme, }) => StreamVideoTheme.raw( textTheme: this.textTheme.merge(textTheme), colorTheme: this.colorTheme.merge(colorTheme), @@ -542,6 +604,12 @@ class StreamVideoTheme extends ThemeExtension { callParticipantsSpotlightTheme, ), livestreamTheme: this.livestreamTheme.merge(livestreamTheme), + incomingCallContentTheme: this.incomingCallContentTheme.merge( + incomingCallContentTheme, + ), + outgoingCallContentTheme: this.outgoingCallContentTheme.merge( + outgoingCallContentTheme, + ), ); /// Merge themes @@ -593,6 +661,12 @@ class StreamVideoTheme extends ThemeExtension { other.callParticipantsSpotlightTheme, ), livestreamTheme: livestreamTheme.merge(other.livestreamTheme), + incomingCallContentTheme: incomingCallContentTheme.merge( + other.incomingCallContentTheme, + ), + outgoingCallContentTheme: outgoingCallContentTheme.merge( + other.outgoingCallContentTheme, + ), ); } @@ -709,6 +783,20 @@ class StreamVideoTheme extends ThemeExtension { ) ?? callParticipantsSpotlightTheme, livestreamTheme: livestreamTheme.lerp(other.livestreamTheme, t), + incomingCallContentTheme: + StreamIncomingCallThemeData.lerp( + incomingCallContentTheme, + other.incomingCallContentTheme, + t, + ) ?? + incomingCallContentTheme, + outgoingCallContentTheme: + StreamOutgoingCallThemeData.lerp( + outgoingCallContentTheme, + other.outgoingCallContentTheme, + t, + ) ?? + outgoingCallContentTheme, ); } } diff --git a/packages/stream_video_flutter/lib/stream_video_flutter.dart b/packages/stream_video_flutter/lib/stream_video_flutter.dart index 7c41894ba..a3895748b 100644 --- a/packages/stream_video_flutter/lib/stream_video_flutter.dart +++ b/packages/stream_video_flutter/lib/stream_video_flutter.dart @@ -8,7 +8,6 @@ library stream_video_flutter; export 'package:stream_core_flutter/core.dart' hide StreamTextTheme; export 'package:stream_video/stream_video.dart'; - export 'src/call_background/background_service.dart'; export 'src/call_background/model/notification_options.dart'; export 'src/call_background/model/notification_payload.dart'; @@ -59,6 +58,7 @@ export 'src/call_screen/call_content/picture_in_picture/picture_in_picture_confi export 'src/call_screen/call_content/picture_in_picture/stream_picture_in_picture_android_view.dart'; export 'src/call_screen/call_content/picture_in_picture/stream_picture_in_picture_ui_kit_view.dart'; export 'src/call_screen/call_content/stream_call_duration_badge.dart'; +export 'src/call_screen/common/ringing_call_background.dart'; export 'src/call_screen/incoming_call/incoming_call_content.dart'; export 'src/call_screen/lobby_actions.dart'; export 'src/call_screen/lobby_actions/lobby_device_menu.dart'; @@ -73,6 +73,7 @@ export 'src/call_screen/lobby_scope.dart'; export 'src/call_screen/lobby_view.dart'; export 'src/call_screen/media_devices_controller.dart'; export 'src/call_screen/outgoing_call/outgoing_call_content.dart'; +export 'src/call_screen/outgoing_call/ringing_camera_controller.dart'; export 'src/call_screen/stream_device_error.dart'; export 'src/components/stream_video_component_builders.dart'; export 'src/livestream/livestream_backstage_content.dart'; diff --git a/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart b/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart index af88d6fc3..5a6d7aeb0 100644 --- a/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart +++ b/packages/stream_video_flutter/test/src/call_controls/call_ringing_button_test.dart @@ -1,7 +1,6 @@ import 'package:alchemist/alchemist.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_core_flutter/core.dart'; import 'package:stream_video_flutter/stream_video_flutter.dart'; import '../../test_utils/goldens.dart'; diff --git a/packages/stream_video_flutter/test/src/call_screen/ringing_call_test.dart b/packages/stream_video_flutter/test/src/call_screen/ringing_call_test.dart new file mode 100644 index 000000000..c7819392c --- /dev/null +++ b/packages/stream_video_flutter/test/src/call_screen/ringing_call_test.dart @@ -0,0 +1,255 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_video_flutter/stream_video_flutter.dart'; + +import '../../test_utils/goldens.dart'; +import '../../test_utils/test_wrapper.dart'; +import '../mocks.dart'; + +void main() { + setUpAll(() { + // The selector `partialState` is called with, which mocktail's `any()` + // needs a stand-in for. + registerFallbackValue( + (CallState state) => + state.ringingMembers.map((it) => it.toUserInfo()).toList(), + ); + }); + + ({MockCall call, MockCallState state}) ringing( + List names, { + CallConnectOptions connectOptions = const CallConnectOptions(), + }) { + final call = MockCall(); + final state = MockCallState(); + stubRingingCall( + call, + state, + ringingMembers: [for (final name in names) ringingMember(name)], + connectOptions: connectOptions, + ); + return (call: call, state: state); + } + + group('StreamIncomingCallContent', () { + testWidgets('names one caller, and says the call is incoming', ( + tester, + ) async { + final mocks = ringing(['Tammy']); + + await tester.pumpWidget( + TestWrapper(child: StreamIncomingCallContent(call: mocks.call)), + ); + + expect(find.text('Tammy'), findsOneWidget); + expect(find.text('Incoming call'), findsOneWidget); + expect(find.text('Accept'), findsOneWidget); + expect(find.text('Decline'), findsOneWidget); + }); + + testWidgets('one caller gets one avatar at the largest size', ( + tester, + ) async { + await tester.pumpWidget( + TestWrapper( + child: StreamIncomingCallContent(call: ringing(['Tammy']).call), + ), + ); + + expect(find.byType(StreamAvatarGroup), findsNothing); + expect( + tester.getSize(find.byType(StreamAvatar)), + Size.square(StreamAvatarSize.xxxl.value), + ); + }); + + testWidgets('more than one caller gets a group in the same box', ( + tester, + ) async { + await tester.pumpWidget( + TestWrapper( + child: StreamIncomingCallContent( + call: ringing(['Tammy', 'Ann']).call, + ), + ), + ); + + expect( + tester.getSize(find.byType(StreamAvatarGroup)), + Size.square(StreamAvatarGroupSize.xxxl.value), + ); + }); + + testWidgets('three callers are named two and a count', (tester) async { + await tester.pumpWidget( + TestWrapper( + child: StreamIncomingCallContent( + call: ringing(['Tammy', 'Ann', 'Bo']).call, + ), + ), + ); + + expect(find.text('Tammy, Ann, and 1 other'), findsOneWidget); + }); + + testWidgets('four callers count the rest in the plural', (tester) async { + await tester.pumpWidget( + TestWrapper( + child: StreamIncomingCallContent( + call: ringing(['Tammy', 'Ann', 'Bo', 'Cass']).call, + ), + ), + ); + + expect(find.text('Tammy, Ann, and 2 others'), findsOneWidget); + }); + + testWidgets( + 'the microphone and camera the call is answered with are kept', + ( + tester, + ) async { + // The design drops these; the SDK keeps them, so this is the test that + // notices if they are lost in a later pass at the screen. + final mocks = ringing( + ['Tammy'], + connectOptions: CallConnectOptions( + microphone: TrackOption.enabled(), + camera: TrackOption.disabled(), + ), + ); + const icons = StreamIcons(); + + await tester.pumpWidget( + TestWrapper(child: StreamIncomingCallContent(call: mocks.call)), + ); + + expect(find.byIcon(icons.voiceFill), findsOneWidget); + expect(find.byIcon(icons.videoOffFill), findsOneWidget); + }, + ); + + testWidgets('a supplied avatar and name replace the default ones', ( + tester, + ) async { + final mocks = ringing(['Tammy']); + + await tester.pumpWidget( + TestWrapper( + child: StreamIncomingCallContent( + call: mocks.call, + participantsAvatarWidgetBuilder: (_, _, _) => + const Text('custom avatar'), + participantsDisplayNameWidgetBuilder: (_, _, _) => + const Text('custom name'), + ), + ), + ); + + expect(find.text('custom avatar'), findsOneWidget); + expect(find.text('custom name'), findsOneWidget); + expect(find.text('Tammy'), findsNothing); + expect(find.byType(StreamAvatar), findsNothing); + }); + }); + + group('StreamOutgoingCallContent', () { + testWidgets('names the callee, and says the call is calling', ( + tester, + ) async { + final mocks = ringing(['Tammy']); + + await tester.pumpWidget( + TestWrapper(child: StreamOutgoingCallContent(call: mocks.call)), + ); + + expect(find.text('Tammy'), findsOneWidget); + expect(find.text('Calling…'), findsOneWidget); + // One large button, and no labels: the outgoing screen only cancels. + expect(find.byType(CallRingingButton), findsOneWidget); + expect(find.text('Decline'), findsNothing); + }); + + testWidgets('the camera reads as off until a track opens', (tester) async { + const icons = StreamIcons(); + final mocks = ringing(['Tammy']); + final controller = StreamRingingCameraController( + call: mocks.call, + openCameraTrack: () async => mockCameraTrack(), + ); + addTearDown(controller.dispose); + + await tester.pumpWidget( + TestWrapper( + child: StreamOutgoingCallContent( + call: mocks.call, + controller: controller, + ), + ), + ); + + expect(find.byIcon(icons.videoOffFill), findsOneWidget); + + // TrackOption.provided reads as neither enabled nor disabled, so the + // screen asks the controller rather than the connect options. + await controller.setCameraEnabled(enabled: true); + await tester.pump(); + + expect(find.byIcon(icons.videoFill), findsOneWidget); + }); + + testWidgets('a background builder replaces the camera behind the blur', ( + tester, + ) async { + final mocks = ringing(['Tammy']); + + await tester.pumpWidget( + TestWrapper( + child: StreamOutgoingCallContent( + call: mocks.call, + callBackgroundWidgetBuilder: (_, _, child) => + ColoredBox(color: const Color(0xFF112233), child: child), + ), + ), + ); + + expect(find.byType(RingingCallBackground), findsNothing); + expect(find.text('Tammy'), findsOneWidget); + }); + }); + + for (final brightness in Brightness.values) { + streamGoldenTest( + 'the ringing screens', + fileName: 'ringing_call', + brightness: brightness, + builder: () => GoldenTestGroup( + columns: 3, + // Both screens fill whatever they are given, so each scenario is + // pinned to a phone-sized box rather than left to expand. + scenarioConstraints: const BoxConstraints.tightFor( + width: 402, + height: 740, + ), + children: [ + GoldenTestScenario( + name: 'incoming', + child: StreamIncomingCallContent(call: ringing(['Tammy']).call), + ), + GoldenTestScenario( + name: 'incoming group', + child: StreamIncomingCallContent( + call: ringing(['Tammy', 'Ann', 'Bo']).call, + ), + ), + GoldenTestScenario( + name: 'outgoing', + child: StreamOutgoingCallContent(call: ringing(['Tammy']).call), + ), + ], + ), + ); + } +} diff --git a/packages/stream_video_flutter/test/src/call_screen/ringing_camera_controller_test.dart b/packages/stream_video_flutter/test/src/call_screen/ringing_camera_controller_test.dart new file mode 100644 index 000000000..7a40ab47e --- /dev/null +++ b/packages/stream_video_flutter/test/src/call_screen/ringing_camera_controller_test.dart @@ -0,0 +1,150 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_video_flutter/stream_video_flutter.dart'; + +import '../mocks.dart'; + +void main() { + setUpAll(() => registerFallbackValue(const CallConnectOptions())); + + late MockCall call; + late MockCallState state; + late CallConnectOptions options; + + setUp(() { + call = MockCall(); + state = MockCallState(); + options = const CallConnectOptions(); + stubRingingCall(call, state); + // The mock has no storage of its own, so the setter is captured and the + // getter reads back what was last written. + when(() => call.connectOptions).thenAnswer((_) => options); + when(() => call.connectOptions = any()).thenAnswer((invocation) { + return options = + invocation.positionalArguments.first as CallConnectOptions; + }); + }); + + StreamRingingCameraController controllerWith({ + MockRtcLocalCameraTrack? track, + Future Function()? open, + }) => StreamRingingCameraController( + call: call, + openCameraTrack: open ?? () async => track ?? mockCameraTrack(), + ); + + test('the camera is handed to the call as a provided track', () async { + final track = mockCameraTrack(); + final controller = controllerWith(track: track); + + await controller.setCameraEnabled(enabled: true); + + expect(controller.cameraEnabled, isTrue); + expect(controller.cameraTrack, track); + expect(options.camera, isA()); + // isEnabled reads false for a provided track, which is why the screen asks + // the controller rather than the option. + expect(options.camera.isEnabled, isFalse); + + controller.dispose(); + }); + + test( + 'turning the camera off stops the track and clears the option', + () async { + final track = mockCameraTrack(); + final controller = controllerWith(track: track); + + await controller.setCameraEnabled(enabled: true); + await controller.setCameraEnabled(enabled: false); + + expect(controller.cameraEnabled, isFalse); + expect(options.camera, isA()); + verify(track.stop).called(1); + + controller.dispose(); + }, + ); + + test( + 'a camera that will not open is reported, and the call carries on', + () async { + final controller = controllerWith( + open: () async => throw Exception('nope'), + ); + + await controller.setCameraEnabled(enabled: true); + + expect(controller.cameraError, isNotNull); + expect(controller.cameraEnabled, isFalse); + expect(options.camera, isA()); + + controller.dispose(); + }, + ); + + test( + 'a track that lands after the camera was turned off is stopped', + () async { + final track = mockCameraTrack(); + final opened = Completer(); + final controller = controllerWith(open: () => opened.future); + + final opening = controller.setCameraEnabled(enabled: true); + await controller.setCameraEnabled(enabled: false); + opened.complete(track); + await opening; + + expect(controller.cameraEnabled, isFalse); + verify(track.stop).called(1); + + controller.dispose(); + }, + ); + + test('a cancelled call turns the camera off on the way out', () async { + final track = mockCameraTrack(); + final controller = controllerWith(track: track); + await controller.setCameraEnabled(enabled: true); + + when( + () => state.status, + ).thenReturn( + CallStatus.disconnected( + const DisconnectReason.cancelled(byUserId: 'local'), + ), + ); + controller.dispose(); + + verify(track.stop).called(1); + }); + + test('a call the callee picked up keeps the camera running', () async { + final track = mockCameraTrack(); + final controller = controllerWith(track: track); + await controller.setCameraEnabled(enabled: true); + + // The call owns the track now — it was handed over as TrackOption.provided + // and stopping it here would cut the video the caller just joined with. + when( + () => state.status, + ).thenReturn(CallStatus.outgoing(acceptedByCallee: true)); + controller.dispose(); + + verifyNever(track.stop); + }); + + test('the microphone is recorded, not opened', () { + final controller = controllerWith(); + + controller.setMicrophoneEnabled(enabled: true); + expect(options.microphone, isA()); + + controller.toggleMicrophone(); + expect(options.microphone, isA()); + + controller.dispose(); + }); +} diff --git a/packages/stream_video_flutter/test/src/mocks.dart b/packages/stream_video_flutter/test/src/mocks.dart index f6e54ea5b..ed07f1425 100644 --- a/packages/stream_video_flutter/test/src/mocks.dart +++ b/packages/stream_video_flutter/test/src/mocks.dart @@ -94,3 +94,59 @@ MutableSharedEmitter stubLobbyCall( return events; } + +/// Stubs what the ringing screens read off a call: the members being rung, the +/// microphone and camera the call would be placed with, and the state stream +/// [PartialCallStateBuilder] follows. +/// +/// [MockCallState.ringingMembers] is a getter over `callMembers`, which a mock +/// does not compute — it is stubbed directly instead. +void stubRingingCall( + MockCall call, + MockCallState state, { + List ringingMembers = const [], + CallConnectOptions connectOptions = const CallConnectOptions(), + CallStatus? status, + UserInfo currentUser = const UserInfo(id: 'local'), +}) { + when(() => state.currentUserId).thenReturn(currentUser.id); + when(() => state.ringingMembers).thenReturn(ringingMembers); + when(() => state.status).thenReturn(status ?? CallStatus.idle()); + when(() => call.currentUser).thenReturn(currentUser); + when(() => call.callCid).thenReturn(StreamCallCid(cid: 'default:ringing')); + when(() => call.connectOptions).thenReturn(connectOptions); + when(() => call.state).thenAnswer( + (_) => MutableStateEmitter(state, sync: true), + ); + // Runs the real selector against the stubbed state, so a screen selecting + // something other than the ringing members still gets what it asked for. + when(() => call.partialState>(any())).thenAnswer((invocation) { + final selector = + invocation.positionalArguments.first + as CallStateSelector>; + return Stream.value(selector(state)); + }); +} + +/// A member of a ringing call, named [name] and with no picture. +CallMemberState ringingMember(String name) => CallMemberState( + userId: name.toLowerCase(), + name: name, + roles: const [], + custom: const {}, +); + +/// A camera track that answers the questions a renderer asks of it. +/// +/// [RtcLocalCameraTrack.mediaConstraints] is what a preview reads the facing +/// mode off, and a bare mock returns null for it. +MockRtcLocalCameraTrack mockCameraTrack({ + FacingMode facingMode = FacingMode.user, +}) { + final track = MockRtcLocalCameraTrack(); + when( + () => track.mediaConstraints, + ).thenReturn(CameraConstraints(facingMode: facingMode)); + when(track.stop).thenAnswer((_) async {}); + return track; +} From 113e33bba574f5021ef8247648d68b396f84f60b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 13:35:39 +0200 Subject: [PATCH 03/11] feat(push): restyle the Android full-screen incoming call The full-screen activity follows the redesigned ringing screen: the app surface rather than the blue default, a 104dp avatar, and 64dp accept and decline buttons centred 80dp apart above the bottom. The text colour no longer defaults to white, so the caller's name, the handle and the action labels each take the colour the design gives them; an integrator's own colour still paints all four. A malformed colour is logged rather than silently ignored. Co-Authored-By: Claude Opus 5 --- .../CHANGELOG.md | 11 +++++ .../IncomingCallActivity.kt | 45 ++++++++++++------- .../activity_incoming_call.xml | 35 +++++++-------- .../res/layout/activity_incoming_call.xml | 39 ++++++++-------- .../android/src/main/res/values/colors.xml | 26 ++++++++--- .../android/src/main/res/values/dimens.xml | 19 +++++--- .../src/stream_video_push_notification.dart | 3 +- 7 files changed, 112 insertions(+), 66 deletions(-) diff --git a/packages/stream_video_push_notification/CHANGELOG.md b/packages/stream_video_push_notification/CHANGELOG.md index bd2a7f381..196df0235 100644 --- a/packages/stream_video_push_notification/CHANGELOG.md +++ b/packages/stream_video_push_notification/CHANGELOG.md @@ -1,3 +1,14 @@ +## Upcoming (major) + +### 🔄 Changed + +- [Android] The full-screen incoming call activity follows the redesigned ringing screen: the app surface instead of the blue `#0955fa`, a 104dp avatar, 64dp accept and decline buttons centred 80dp apart above the bottom, and the design system's `#00A46E` and `#D90D10` in place of the Material green and red. An app that sets `IncomingCallNotificationParams` keeps whatever it set. +- [Android] `IncomingCallNotificationParams.fullScreenTextColor` no longer defaults to white. Left unset, the caller's name, the handle under it and the action labels each take the colour the design gives them rather than all four being painted the same. Setting it still paints all four. +- [Android] `IncomingCallNotificationParams.fullScreenBackgroundColor` no longer defaults to `#0955fa` on the Dart side. The default is the plugin's own resource, so it can be overridden by an app's `colors.xml` as well as through the params. +- [Android] A malformed colour in `IncomingCallNotificationParams` is logged rather than silently ignored. It still falls back to the default. + + The heads-up notification is unchanged. From API 34 it is drawn by the platform's `NotificationCompat.CallStyle`, whose buttons and labels are the system's — `textAccept` and `textDecline` do not reach it. iOS ringing is CallKit, which is system UI throughout and has nothing to restyle. + ## 1.6.0 ### 🔄 Changed diff --git a/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/IncomingCallActivity.kt b/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/IncomingCallActivity.kt index c4c73bfe2..eb791e4a5 100644 --- a/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/IncomingCallActivity.kt +++ b/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/IncomingCallActivity.kt @@ -26,11 +26,14 @@ import android.view.ViewGroup.MarginLayoutParams import android.os.PowerManager import android.text.TextUtils import android.util.Log +import androidx.core.content.ContextCompat class IncomingCallActivity : Activity() { companion object { + private const val TAG = "IncomingCallActivity" + private const val ACTION_ENDED_CALL_INCOMING = "io.getstream.video.ACTION_ENDED_CALL_INCOMING" @@ -159,6 +162,17 @@ class IncomingCallActivity : Activity() { } + /** [color] as an ARGB int, or null when it is absent or malformed. */ + private fun parseColorOrNull(color: String?): Int? { + if (color.isNullOrEmpty()) return null + return try { + Color.parseColor(color) + } catch (error: IllegalArgumentException) { + Log.w(TAG, "Ignoring unparseable colour \"$color\"", error) + null + } + } + private fun incomingData(intent: Intent) { val data = intent.extras?.getBundle(IncomingCallConstants.EXTRA_CALL_INCOMING_DATA) if (data == null) finish() @@ -173,16 +187,20 @@ class IncomingCallActivity : Activity() { } } - val textColor = data?.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_TEXT_COLOR, "#ffffff") + // No default: without a colour from the integrator every label keeps + // the one the layout gives it, and the design does not paint the + // caller's name, the handle and the action labels the same. + val textColor = parseColorOrNull( + data?.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_TEXT_COLOR) + ) val showCallHandle = data?.getBoolean(IncomingCallConstants.EXTRA_CALL_SHOW_CALL_HANDLE, false) tvCallerName.text = data?.getString(IncomingCallConstants.EXTRA_CALL_NAME_CALLER, "") tvNumber.text = data?.getString(IncomingCallConstants.EXTRA_CALL_HANDLE, "") tvNumber.visibility = if (showCallHandle == true) View.VISIBLE else View.INVISIBLE - try { - tvCallerName.setTextColor(Color.parseColor(textColor)) - tvNumber.setTextColor(Color.parseColor(textColor)) - } catch (error: Exception) { + if (textColor != null) { + tvCallerName.setTextColor(textColor) + tvNumber.setTextColor(textColor) } val showLogo = data?.getBoolean(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_SHOW_LOGO, false) @@ -230,18 +248,15 @@ class IncomingCallActivity : Activity() { tvDecline.text = if (TextUtils.isEmpty(textDecline)) getString(R.string.text_decline) else textDecline - try { - tvAccept.setTextColor(Color.parseColor(textColor)) - tvDecline.setTextColor(Color.parseColor(textColor)) - } catch (error: Exception) { + if (textColor != null) { + tvAccept.setTextColor(textColor) + tvDecline.setTextColor(textColor) } - val backgroundColor = - data?.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_BACKGROUND_COLOR, "#0955fa") - try { - ivBackground.setBackgroundColor(Color.parseColor(backgroundColor)) - } catch (error: Exception) { - } + val backgroundColor = parseColorOrNull( + data?.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_BACKGROUND_COLOR) + ) ?: ContextCompat.getColor(this, R.color.incoming_call_background) + ivBackground.setBackgroundColor(backgroundColor) var backgroundUrl = data?.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_BACKGROUND_URL, "") if (!backgroundUrl.isNullOrEmpty()) { if (!backgroundUrl.startsWith("http://", true) && !backgroundUrl.startsWith( diff --git a/packages/stream_video_push_notification/android/src/main/res/layout-w600dp-land/activity_incoming_call.xml b/packages/stream_video_push_notification/android/src/main/res/layout-w600dp-land/activity_incoming_call.xml index a956455a0..a631ab0da 100644 --- a/packages/stream_video_push_notification/android/src/main/res/layout-w600dp-land/activity_incoming_call.xml +++ b/packages/stream_video_push_notification/android/src/main/res/layout-w600dp-land/activity_incoming_call.xml @@ -10,7 +10,7 @@ android:id="@+id/ivBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#0955fa" + android:background="@color/incoming_call_background" android:scaleType="centerCrop" tools:ignore="ContentDescription" /> @@ -50,7 +50,7 @@ android:visibility="invisible" android:layout_centerInParent="true" android:src="@drawable/ic_default_avatar" - app:civ_border_color="#80ffffff" + app:civ_border_color="@color/incoming_call_avatar_border" app:civ_border_width="1dp" /> @@ -73,7 +73,7 @@ android:autoSizeTextType="uniform" android:ellipsize="end" android:maxLines="1" - android:textColor="@android:color/white" + android:textColor="@color/incoming_call_text_primary" android:textSize="@dimen/size_text_name" app:autoSizeMaxTextSize="@dimen/size_text_name" app:autoSizeMinTextSize="12sp" @@ -90,8 +90,8 @@ android:layout_marginTop="4dp" android:ellipsize="end" android:maxLines="1" - android:textColor="@color/action_text" - android:textSize="@dimen/size_text_action" + android:textColor="@color/incoming_call_text_secondary" + android:textSize="@dimen/size_text_status" tools:text="Some info" /> @@ -104,7 +104,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:layout_marginBottom="0dp" + android:layout_marginBottom="@dimen/size_action_bottom" android:fitsSystemWindows="true" android:gravity="center" android:orientation="horizontal"> @@ -116,8 +116,8 @@ tools:ignore="UseCompoundDrawables"> @@ -135,12 +135,12 @@ @@ -148,20 +148,19 @@ @@ -180,12 +179,12 @@ diff --git a/packages/stream_video_push_notification/android/src/main/res/layout/activity_incoming_call.xml b/packages/stream_video_push_notification/android/src/main/res/layout/activity_incoming_call.xml index 44e34c823..dc7143679 100644 --- a/packages/stream_video_push_notification/android/src/main/res/layout/activity_incoming_call.xml +++ b/packages/stream_video_push_notification/android/src/main/res/layout/activity_incoming_call.xml @@ -10,7 +10,7 @@ android:id="@+id/ivBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#0955fa" + android:background="@color/incoming_call_background" android:scaleType="centerCrop" tools:ignore="ContentDescription" /> @@ -50,7 +50,7 @@ android:visibility="invisible" android:layout_centerInParent="true" android:src="@drawable/ic_default_avatar" - app:civ_border_color="#80ffffff" + app:civ_border_color="@color/incoming_call_avatar_border" app:civ_border_width="1dp" /> @@ -73,7 +73,7 @@ android:autoSizeTextType="uniform" android:ellipsize="end" android:maxLines="1" - android:textColor="@android:color/white" + android:textColor="@color/incoming_call_text_primary" android:textSize="@dimen/size_text_name" app:autoSizeMaxTextSize="@dimen/size_text_name" app:autoSizeMinTextSize="12sp" @@ -90,8 +90,8 @@ android:layout_marginTop="4dp" android:ellipsize="end" android:maxLines="1" - android:textColor="@color/action_text" - android:textSize="@dimen/size_text_action" + android:textColor="@color/incoming_call_text_secondary" + android:textSize="@dimen/size_text_status" tools:text="Some info" /> @@ -109,8 +109,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:layout_marginBottom="0dp" + android:layout_marginBottom="@dimen/size_action_bottom" android:fitsSystemWindows="true" + android:gravity="center" android:orientation="horizontal"> @@ -139,12 +140,12 @@ @@ -152,21 +153,19 @@ + android:layout_width="@dimen/size_action_gap" + android:layout_height="0dp" /> @@ -185,12 +184,12 @@ diff --git a/packages/stream_video_push_notification/android/src/main/res/values/colors.xml b/packages/stream_video_push_notification/android/src/main/res/values/colors.xml index e7cad1edd..d6136e8b9 100644 --- a/packages/stream_video_push_notification/android/src/main/res/values/colors.xml +++ b/packages/stream_video_push_notification/android/src/main/res/values/colors.xml @@ -1,11 +1,25 @@ - #4CAF50 - #A6FFA9 - #F44336 - #FB8D85 + + #00A46E + #8000A46E + #D90D10 + #80D90D10 + #FFFFFF - #80ffffff - \ No newline at end of file + + + #FFFFFF + #1A1B25 + #414552 + #1A1A1B25 + + + #141A1B25 + diff --git a/packages/stream_video_push_notification/android/src/main/res/values/dimens.xml b/packages/stream_video_push_notification/android/src/main/res/values/dimens.xml index 7c5cdee9b..f9736540e 100644 --- a/packages/stream_video_push_notification/android/src/main/res/values/dimens.xml +++ b/packages/stream_video_push_notification/android/src/main/res/values/dimens.xml @@ -14,12 +14,19 @@ 60dp -50dp - 120dp - 60dp - 120dp + 104dp + 64dp + 128dp 150dp - 24sp - 14sp + + 16dp + 104dp + 12dp + + 22sp + 17sp + 15sp 12sp - \ No newline at end of file + diff --git a/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart b/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart index 0589975ca..20c2b058d 100644 --- a/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart +++ b/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart @@ -603,9 +603,10 @@ const _defaultPushConfiguration = StreamVideoPushConfiguration( subtitle: 'Missed call', callbackText: 'Call back', ), + // No colours: the plugin's own resources carry the design's, and naming + // one here would mean the default could only be changed in two places. incomingCallNotification: IncomingCallNotificationParams( fullScreenShowLogo: false, - fullScreenBackgroundColor: '#0955fa', ), ), ios: IOSPushConfiguration( From 0a9dad4b9ea2e8251683a0cb1ac545f172e5e079 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 13:42:52 +0200 Subject: [PATCH 04/11] refactor(ui): let the type carry the ringing screens' resolved style The internal pieces took a StreamRingingCallStyle, whose every field is nullable, and force-unwrapped what the screen had already resolved. They take the resolved defaults instead, so nothing can hand them a style full of nulls. RingingCallBackground stays public and resolves its own, since a callBackgroundWidgetBuilder has no resolved style to pass. Co-Authored-By: Claude Opus 5 --- .../common/ringing_call_background.dart | 19 ++++++++----- .../common/ringing_call_details.dart | 9 +++--- .../common/ringing_call_style_defaults.dart | 28 +++++++++++++++---- .../incoming_call/incoming_call_content.dart | 24 +--------------- .../incoming_call/incoming_call_controls.dart | 7 +++-- .../outgoing_call/outgoing_call_content.dart | 7 ++--- .../outgoing_call/outgoing_call_controls.dart | 7 +++-- 7 files changed, 50 insertions(+), 51 deletions(-) diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart index c58b626ec..235d0fef3 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_background.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; +import 'ringing_call_style_defaults.dart'; /// What an outgoing ringing screen is drawn on: the caller's own camera, /// blurred and washed out so the call details stay legible on top of it. @@ -14,13 +15,16 @@ class RingingCallBackground extends StatelessWidget { /// Creates a new instance of [RingingCallBackground]. const RingingCallBackground({ super.key, - required this.style, + this.style, this.cameraTrack, required this.child, }); - /// The resolved style of the screen. - final StreamRingingCallStyle style; + /// Overrides for the fill, the scrim and the blur. + /// + /// Anything left null falls back to what the outgoing screen resolves, so a + /// background built by hand does not have to name all three. + final StreamRingingCallStyle? style; /// The camera to draw, or null to draw the fill alone. final RtcLocalCameraTrack? cameraTrack; @@ -30,12 +34,13 @@ class RingingCallBackground extends StatelessWidget { @override Widget build(BuildContext context) { + final style = RingingCallStyleDefaults(context, this.style); final track = cameraTrack; return Stack( fit: StackFit.expand, children: [ - ColoredBox(color: style.backgroundColor!), + ColoredBox(color: style.backgroundColor), if (track != null) VideoTrackRenderer( videoTrack: track, @@ -47,10 +52,10 @@ class RingingCallBackground extends StatelessWidget { ClipRect( child: BackdropFilter( filter: ImageFilter.blur( - sigmaX: style.blurSigma!, - sigmaY: style.blurSigma!, + sigmaX: style.blurSigma, + sigmaY: style.blurSigma, ), - child: ColoredBox(color: style.scrimColor!), + child: ColoredBox(color: style.scrimColor), ), ), child, diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart index 30af134fc..74b4d519f 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_details.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; import '../../l10n/localization_extension.dart'; +import 'ringing_call_style_defaults.dart'; /// Who is ringing and what the call is doing: an avatar over a name and a /// status line. @@ -26,7 +27,7 @@ class RingingCallDetails extends StatelessWidget { final String status; /// The resolved style of the screen this block sits on. - final StreamRingingCallStyle style; + final RingingCallStyleDefaults style; /// Drawn in place of the avatar, when the host supplied one. final Widget? avatar; @@ -38,12 +39,12 @@ class RingingCallDetails extends StatelessWidget { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - spacing: style.contentSpacing!, + spacing: style.contentSpacing, children: [ avatar ?? _buildAvatar(), Column( mainAxisSize: MainAxisSize.min, - spacing: style.titleSpacing!, + spacing: style.titleSpacing, children: [ nameLine ?? Text( @@ -61,7 +62,7 @@ class RingingCallDetails extends StatelessWidget { Widget _buildAvatar() { if (participants.length == 1) { return StreamAvatarTheme( - data: style.avatarTheme!, + data: style.avatarTheme, child: StreamUserAvatar(user: participants.first), ); } diff --git a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart index dab7d278c..27f946f5d 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/common/ringing_call_style_defaults.dart @@ -6,14 +6,16 @@ import '../../../stream_video_flutter.dart'; /// The values a ringing screen falls back to when neither its theme nor its /// call site names one. /// -/// Both screens share a layout and differ in their colors, so the geometry -/// resolves here and each screen overrides the handful of properties that -/// depend on what it is drawn on top of. +/// Both screens share a layout and differ in their colors, so everything +/// resolves here and the outgoing screen overrides the two properties that +/// depend on it being drawn on top of the camera rather than on a surface. /// -/// Never hand an instance of this to a theme: every getter is non-null, so -/// merging one would pin every property of whatever it was merged into. +/// Every getter is non-null, which is what lets the widgets below take one of +/// these rather than a style full of nulls. For the same reason it must never +/// be handed to a theme: merging one would pin every property of whatever it +/// was merged into. @internal -abstract class RingingCallStyleDefaults extends StreamRingingCallStyle { +class RingingCallStyleDefaults extends StreamRingingCallStyle { /// Resolves a ringing screen's defaults from the theme on the given context. RingingCallStyleDefaults(this.context, this.style); @@ -27,6 +29,20 @@ abstract class RingingCallStyleDefaults extends StreamRingingCallStyle { late final textTheme = context.streamTextTheme; late final spacing = context.streamSpacing; + @override + Color get backgroundColor => + style?.backgroundColor ?? colorScheme.backgroundApp; + + @override + TextStyle get titleTextStyle => + style?.titleTextStyle ?? + textTheme.headingLg.copyWith(color: colorScheme.textPrimary); + + @override + TextStyle get statusTextStyle => + style?.statusTextStyle ?? + textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary); + @override Color get scrimColor => style?.scrimColor ?? colorScheme.backgroundScrim; diff --git a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart index 7a5df16f7..7af170cd5 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart @@ -60,7 +60,7 @@ class _StreamIncomingCallContentState extends State { @override Widget build(BuildContext context) { - final style = _StreamIncomingCallStyleDefaults( + final style = RingingCallStyleDefaults( context, StreamIncomingCallTheme.of(context).style?.merge(widget.style) ?? widget.style, @@ -157,25 +157,3 @@ class _StreamIncomingCallContentState extends State { } } } - -// Default style values for [StreamIncomingCallContent]. -// -// The screen sits on a surface of its own, so its text is the ordinary text -// of the app rather than text drawn on top of something. -class _StreamIncomingCallStyleDefaults extends RingingCallStyleDefaults { - _StreamIncomingCallStyleDefaults(super.context, super.style); - - @override - Color get backgroundColor => - style?.backgroundColor ?? colorScheme.backgroundApp; - - @override - TextStyle get titleTextStyle => - style?.titleTextStyle ?? - textTheme.headingLg.copyWith(color: colorScheme.textPrimary); - - @override - TextStyle get statusTextStyle => - style?.statusTextStyle ?? - textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary); -} diff --git a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart index 38b4f6f98..851ed2ccb 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_controls.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; import '../../l10n/localization_extension.dart'; +import '../common/ringing_call_style_defaults.dart'; /// The controls of the incoming ringing screen: answer and decline, over the /// microphone and camera the call will be joined with. @@ -19,7 +20,7 @@ class IncomingCallControls extends StatelessWidget { }); /// The resolved style of the screen these controls sit on. - final StreamRingingCallStyle style; + final RingingCallStyleDefaults style; /// If camera is enabled. final bool isCameraEnabled; @@ -46,12 +47,12 @@ class IncomingCallControls extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, - spacing: style.secondaryControlsSpacing!, + spacing: style.secondaryControlsSpacing, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - spacing: style.controlsSpacing!, + spacing: style.controlsSpacing, children: [ CallRingingButton( icon: Icon(icons.phoneDownFill), diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart index 4761a2558..82a776dfb 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart @@ -212,14 +212,11 @@ class _StreamOutgoingCallContentState extends State { // Default style values for [StreamOutgoingCallContent]. // // The screen is drawn on top of the caller's own camera, so its text is the -// text used on an image rather than on a surface. +// text used on an image rather than on a surface. Everything else is what a +// ringing screen resolves anyway. class _StreamOutgoingCallStyleDefaults extends RingingCallStyleDefaults { _StreamOutgoingCallStyleDefaults(super.context, super.style); - @override - Color get backgroundColor => - style?.backgroundColor ?? colorScheme.backgroundApp; - @override TextStyle get titleTextStyle => style?.titleTextStyle ?? diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart index 1459c1d61..e5f9f1762 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_controls.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../stream_video_flutter.dart'; +import '../common/ringing_call_style_defaults.dart'; /// The controls of the outgoing ringing screen: the microphone and camera the /// call will be placed with, over the button that cancels it. @@ -17,7 +18,7 @@ class OutgoingCallControls extends StatelessWidget { }); /// The resolved style of the screen these controls sit on. - final StreamRingingCallStyle style; + final RingingCallStyleDefaults style; /// If camera is enabled. final bool isCameraEnabled; @@ -40,11 +41,11 @@ class OutgoingCallControls extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, - spacing: style.secondaryControlsSpacing!, + spacing: style.secondaryControlsSpacing, children: [ Row( mainAxisSize: MainAxisSize.min, - spacing: style.controlsSpacing!, + spacing: style.controlsSpacing, children: [ CallControlButton( icon: Icon( From 8ccb3c34e8d691b44e93f533d13a99590c53c696 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 13:54:29 +0200 Subject: [PATCH 05/11] chore(repo): pin stream_core_flutter to the 104px avatar sizes Co-Authored-By: Claude Opus 5 --- pubspec.lock | 4 ++-- pubspec.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1f9f50ec8..96b556075 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1967,8 +1967,8 @@ packages: dependency: "direct overridden" description: path: "packages/stream_core_flutter" - ref: f4d49e9db343b48d890247fc9bf142805804daf1 - resolved-ref: f4d49e9db343b48d890247fc9bf142805804daf1 + ref: "2daa0a641519abe9ab637bcb41c69ed3ea4273b7" + resolved-ref: "2daa0a641519abe9ab637bcb41c69ed3ea4273b7" url: "https://github.com/GetStream/stream-core-flutter.git" source: git version: "0.5.1" diff --git a/pubspec.yaml b/pubspec.yaml index 9d0e9e338..5efcf3669 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dev_dependencies: dependency_overrides: cli_util: ^0.5.1 file_picker: ^12.0.0-beta.5 - # Same commit as stream_core_flutter below: the published 0.5.0 predates + # A git commit rather than a release: the published 0.5.0 predates # CurrentPlatform.debugCurrentPlatformOverride, which the tests need. stream_core: git: @@ -36,7 +36,7 @@ dependency_overrides: git: url: https://github.com/GetStream/stream-core-flutter.git path: packages/stream_core_flutter - ref: f4d49e9db343b48d890247fc9bf142805804daf1 + ref: 2daa0a641519abe9ab637bcb41c69ed3ea4273b7 melos: ignore: From bec0315992319db0695ccc5ddaef7ac19a6970fb Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:57:57 +0000 Subject: [PATCH 06/11] chore: update goldens --- .../goldens/ci/call_ringing_button_dark.png | Bin 0 -> 3650 bytes .../goldens/ci/call_ringing_button_light.png | Bin 0 -> 3945 bytes .../goldens/ci/ringing_call_dark.png | Bin 0 -> 23890 bytes .../goldens/ci/ringing_call_light.png | Bin 0 -> 29429 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_ringing_button_dark.png create mode 100644 packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_ringing_button_light.png create mode 100644 packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_dark.png create mode 100644 packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_light.png diff --git a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_ringing_button_dark.png b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_ringing_button_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..340800f1fee1e903846475beab10cb88f8c008d1 GIT binary patch literal 3650 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 ZP~cY-d_R7g1Nc|QQ&rXk*C|@Q`xgwdjlKW? literal 0 HcmV?d00001 diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_dark.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5b18b76b2e582e1fd7a3061669e21fe8bc52cdc4 GIT binary patch literal 23890 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 literal 0 HcmV?d00001 diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_light.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_light.png new file mode 100644 index 0000000000000000000000000000000000000000..c20d660ccbeb83e3b5f74b20b1cf4823d3eda576 GIT binary patch literal 29429 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 xRqoN4U#OEHLqe+-Qc>lZO+OJHhKh|8WUQ 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 Date: Fri, 11 Sep 2026 14:21:35 +0200 Subject: [PATCH 07/11] chore(repo): override the chat packages with the avatar size fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 104px sizes added to stream_core_flutter stop 10.4.0 compiling, whose switches over the size enums are exhaustive. Only the dogfooding app pulls chat in, so the SDK packages and CI were unaffected — the app was not. stream_chat and stream_chat_flutter_core share its ref so the three stay in step. Drop the overrides once a chat release carries the fix. Co-Authored-By: Claude Opus 5 --- pubspec.lock | 76 +++++++++++++++++----------------------------------- pubspec.yaml | 19 +++++++++++++ 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 96b556075..138e57cbb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1931,28 +1931,31 @@ packages: source: hosted version: "2.1.4" stream_chat: - dependency: transitive + dependency: "direct overridden" description: - name: stream_chat - sha256: bde4a4174b1450546a183ed3e255617d0aff5c074398c6b3919aaee78f76bc8f - url: "https://pub.dev" - source: hosted + path: "packages/stream_chat" + ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + url: "https://github.com/GetStream/stream-chat-flutter.git" + source: git version: "10.4.0" stream_chat_flutter: - dependency: transitive + dependency: "direct overridden" description: - name: stream_chat_flutter - sha256: "8d15749847498481e67e10ef71cb9400726881e78eee9a8322c04f17634ab426" - url: "https://pub.dev" - source: hosted + path: "packages/stream_chat_flutter" + ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + url: "https://github.com/GetStream/stream-chat-flutter.git" + source: git version: "10.4.0" stream_chat_flutter_core: - dependency: transitive + dependency: "direct overridden" description: - name: stream_chat_flutter_core - sha256: "4bbfe3f50150cb51b230abd06b1eded7dd7feea0548ccc57bef48a23aa14f9ec" - url: "https://pub.dev" - source: hosted + path: "packages/stream_chat_flutter_core" + ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + url: "https://github.com/GetStream/stream-chat-flutter.git" + source: git version: "10.4.0" stream_core: dependency: "direct overridden" @@ -1975,11 +1978,12 @@ packages: stream_thumbnail: dependency: transitive description: - name: stream_thumbnail - sha256: "14f562a5b25fb8d037ff34a182a4d7b99b16e0fc268392e3bb2754c964ba4069" - url: "https://pub.dev" - source: hosted - version: "0.1.0" + path: "packages/stream_thumbnail" + ref: "2aea1413997af886ee951921d71e6b8cb8c5cfbb" + resolved-ref: "2aea1413997af886ee951921d71e6b8cb8c5cfbb" + url: "https://github.com/GetStream/stream-core-flutter.git" + source: git + version: "0.1.0+1" stream_transform: dependency: transitive description: @@ -2076,38 +2080,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" - thumblr: - dependency: transitive - description: - name: thumblr - sha256: bfdb4c3d6e95d9595d4d464b255b85845db5f7ab88d8648d3edfe4f19adf043b - url: "https://pub.dev" - source: hosted - version: "0.0.4" - thumblr_macos: - dependency: transitive - description: - name: thumblr_macos - sha256: "3b5189db149ee4c6c2fabc6f4bf3d84ed09c96cc45a2153a53ae781742c404c6" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - thumblr_platform_interface: - dependency: transitive - description: - name: thumblr_platform_interface - sha256: abfcd54b5567b3c8d9a5426b43ed4783c613c34194564c47ffba0ea51f328962 - url: "https://pub.dev" - source: hosted - version: "0.3.1+1" - thumblr_windows: - dependency: transitive - description: - name: thumblr_windows - sha256: a1d372ccb4d4981e6d55ff516ee5b102f65d0f905d5d36c9712a095358d9e91a - url: "https://pub.dev" - source: hosted - version: "0.0.3+1" tint: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5efcf3669..b7f3ff98d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,25 @@ dependency_overrides: url: https://github.com/GetStream/stream-core-flutter.git path: packages/stream_core_flutter ref: 2daa0a641519abe9ab637bcb41c69ed3ea4273b7 + # Until a release carries the fix: the 104px avatar sizes added to + # stream_core_flutter stop 10.4.0 compiling, whose size switches are + # exhaustive. Only the dogfooding app pulls chat in. The chat packages share + # one ref so they stay in step. + stream_chat: + git: + url: https://github.com/GetStream/stream-chat-flutter.git + path: packages/stream_chat + ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 + stream_chat_flutter: + git: + url: https://github.com/GetStream/stream-chat-flutter.git + path: packages/stream_chat_flutter + ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 + stream_chat_flutter_core: + git: + url: https://github.com/GetStream/stream-chat-flutter.git + path: packages/stream_chat_flutter_core + ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 melos: ignore: From 55796ca39cecb3f36d89411670d77aa49a7f9149 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 14:54:22 +0200 Subject: [PATCH 08/11] fix(push): let the Android incoming screen's own colours apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The restyle gave IncomingCallActivity resource defaults to fall back to, but Call.kt still defaulted fullScreenBackgroundColor to #0955fa and fullScreenTextColor to white, so the bundle always carried a colour and the resources were never reached — the full-screen UI stayed blue. They default to empty now, which is what the activity reads as "unset". Co-Authored-By: Claude Opus 5 --- .../flutter/stream_video_push_notification/Call.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/Call.kt b/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/Call.kt index 6f1beee46..a27a1ecda 100644 --- a/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/Call.kt +++ b/packages/stream_video_push_notification/android/src/main/kotlin/io/getstream/video/flutter/stream_video_push_notification/Call.kt @@ -62,14 +62,16 @@ data class Data(val args: Map) { @JsonProperty("ringtonePath") var ringtonePath: String + // Empty rather than a colour: unset lets IncomingCallActivity fall back to + // the plugin's own resources, which an app can override in its colors.xml. @JsonProperty("fullScreenBackgroundColor") - var fullScreenBackgroundColor: String = "#0955fa" + var fullScreenBackgroundColor: String = "" @JsonProperty("fullScreenBackgroundUrl") var fullScreenBackgroundUrl: String = "" @JsonProperty("fullScreenTextColor") - var fullScreenTextColor: String = "#FFFFFF" + var fullScreenTextColor: String = "" @JsonProperty("incomingCallNotificationChannelName") var incomingCallNotificationChannelName: String? = null @@ -148,9 +150,9 @@ data class Data(val args: Map) { if (incomingNotification != null) { fullScreenShowLogo = incomingNotification["fullScreenShowLogo"] as? Boolean ?: false fullScreenLogoUrl = incomingNotification["fullScreenLogoUrl"] as? String? ?: "" - fullScreenBackgroundColor = incomingNotification["fullScreenBackgroundColor"] as? String ?: "#0955fa" + fullScreenBackgroundColor = incomingNotification["fullScreenBackgroundColor"] as? String ?: "" fullScreenBackgroundUrl = incomingNotification["fullScreenBackgroundUrl"] as? String ?: "" - fullScreenTextColor = incomingNotification["fullScreenTextColor"] as? String ?: "#ffffff" + fullScreenTextColor = incomingNotification["fullScreenTextColor"] as? String ?: "" textAccept = incomingNotification["textAccept"] as? String ?: "" textDecline = incomingNotification["textDecline"] as? String ?: "" showCallHandle = incomingNotification["showCallHandle"] as? Boolean ?: false @@ -315,14 +317,14 @@ data class Data(val args: Map) { ) data.fullScreenBackgroundColor = bundle.getString( IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_BACKGROUND_COLOR, - "#0955fa" + "" ) data.fullScreenBackgroundUrl = bundle.getString(IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_BACKGROUND_URL, "") data.fullScreenTextColor = bundle.getString( IncomingCallConstants.EXTRA_CALL_FULL_SCREEN_TEXT_COLOR, - "#FFFFFF" + "" ) data.from = bundle.getString(IncomingCallConstants.EXTRA_CALL_ACTION_FROM, "") From 26366cbb3b1e8dd80b9750697ba86a6ae380157f Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Sep 2026 09:37:53 +0200 Subject: [PATCH 09/11] update dependencies --- .../flutter/generated_plugin_registrant.cc | 4 ++ .../linux/flutter/generated_plugins.cmake | 1 + dogfooding/pubspec.yaml | 4 +- .../windows/flutter/generated_plugins.cmake | 2 +- .../stream_video_flutter/example/pubspec.yaml | 4 +- .../example/pubspec.yaml | 2 +- .../example/pubspec.yaml | 2 +- .../stream_video_screen_sharing/pubspec.yaml | 4 +- pubspec.lock | 66 ++++++++++++++++--- pubspec.yaml | 37 ++++++----- 10 files changed, 90 insertions(+), 36 deletions(-) diff --git a/dogfooding/linux/flutter/generated_plugin_registrant.cc b/dogfooding/linux/flutter/generated_plugin_registrant.cc index 0b1a62a89..ba0ff92aa 100644 --- a/dogfooding/linux/flutter/generated_plugin_registrant.cc +++ b/dogfooding/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) stream_thumbnail_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "StreamThumbnailPlugin"); + stream_thumbnail_plugin_register_with_registrar(stream_thumbnail_registrar); g_autoptr(FlPluginRegistrar) stream_webrtc_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); flutter_web_r_t_c_plugin_register_with_registrar(stream_webrtc_flutter_registrar); diff --git a/dogfooding/linux/flutter/generated_plugins.cmake b/dogfooding/linux/flutter/generated_plugins.cmake index 9c93bb9d2..1c81b07d8 100644 --- a/dogfooding/linux/flutter/generated_plugins.cmake +++ b/dogfooding/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop file_selector_linux record_linux + stream_thumbnail stream_webrtc_flutter url_launcher_linux ) diff --git a/dogfooding/pubspec.yaml b/dogfooding/pubspec.yaml index 2fbce0e48..bd20797f2 100644 --- a/dogfooding/pubspec.yaml +++ b/dogfooding/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: none description: Flutter Dogfooding App to showcase Video SDK. environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + sdk: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" resolution: workspace diff --git a/dogfooding/windows/flutter/generated_plugins.cmake b/dogfooding/windows/flutter/generated_plugins.cmake index 8334b1656..2cd7f278c 100644 --- a/dogfooding/windows/flutter/generated_plugins.cmake +++ b/dogfooding/windows/flutter/generated_plugins.cmake @@ -13,8 +13,8 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows record_windows share_plus + stream_thumbnail stream_webrtc_flutter - thumblr_windows url_launcher_windows ) diff --git a/packages/stream_video_flutter/example/pubspec.yaml b/packages/stream_video_flutter/example/pubspec.yaml index f939f580c..378840f69 100644 --- a/packages/stream_video_flutter/example/pubspec.yaml +++ b/packages/stream_video_flutter/example/pubspec.yaml @@ -8,8 +8,8 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.1+1 environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + sdk: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" resolution: workspace diff --git a/packages/stream_video_noise_cancellation/example/pubspec.yaml b/packages/stream_video_noise_cancellation/example/pubspec.yaml index 1d3e4c22b..f4c065d1c 100644 --- a/packages/stream_video_noise_cancellation/example/pubspec.yaml +++ b/packages/stream_video_noise_cancellation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 0.1.0 environment: - sdk: ">=3.10.0 <4.0.0" + sdk: ">=3.12.0 <4.0.0" resolution: workspace diff --git a/packages/stream_video_push_notification/example/pubspec.yaml b/packages/stream_video_push_notification/example/pubspec.yaml index f7af4a05c..74fa52600 100644 --- a/packages/stream_video_push_notification/example/pubspec.yaml +++ b/packages/stream_video_push_notification/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 0.1.0 environment: - sdk: ">=3.10.0 <4.0.0" + sdk: ">=3.12.0 <4.0.0" resolution: workspace diff --git a/packages/stream_video_screen_sharing/pubspec.yaml b/packages/stream_video_screen_sharing/pubspec.yaml index ba9303b88..870f2e028 100644 --- a/packages/stream_video_screen_sharing/pubspec.yaml +++ b/packages/stream_video_screen_sharing/pubspec.yaml @@ -8,8 +8,8 @@ repository: https://github.com/GetStream/stream-video-flutter issue_tracker: https://github.com/GetStream/stream-video-flutter/issues environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + sdk: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" resolution: workspace diff --git a/pubspec.lock b/pubspec.lock index 138e57cbb..178c83173 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "12.1.0" + android_file_picker: + dependency: transitive + description: + name: android_file_picker + sha256: "14ab27769b54c48d5a8a71aa9858372b7a3ae77572ea0a8aee744643c5d8c53d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" ansi_styles: dependency: transitive description: @@ -498,13 +506,45 @@ packages: source: hosted version: "7.0.1" file_picker: - dependency: "direct overridden" + dependency: transitive description: name: file_picker - sha256: fdc6a37f715d19f35b131decf1ce39242eeed5ddae18c0818c3eccb731ab76be + sha256: "98c0b156b6380ba55bc767a14e2e6b3feb246e713ad40092424596fa79005bea" + url: "https://pub.dev" + source: hosted + version: "13.1.0" + file_picker_darwin: + dependency: transitive + description: + name: file_picker_darwin + sha256: "51aef9f4c80449c736e7cc02198675fda73689f0bd45a84da642cfeab0250d46" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file_picker_linux: + dependency: transitive + description: + name: file_picker_linux + sha256: e7db600f50672ce5ebbe422ac0906e0f3d2a476188cc93e7126afee12bf08063 url: "https://pub.dev" source: hosted - version: "12.0.0-beta.7" + version: "2.0.0" + file_picker_platform_interface: + dependency: transitive + description: + name: file_picker_platform_interface + sha256: bbdc085a6f168e63f147e9ce8988f79fa0800a58e7f25a48f762c60837107ba5 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + file_picker_web: + dependency: transitive + description: + name: file_picker_web + sha256: b3004268da0c1b52baa18df430e7ec4438194de855a807d26b447cbf07ef3496 + url: "https://pub.dev" + source: hosted + version: "4.0.0" file_selector: dependency: transitive description: @@ -1934,8 +1974,8 @@ packages: dependency: "direct overridden" description: path: "packages/stream_chat" - ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" - resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" + resolved-ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" url: "https://github.com/GetStream/stream-chat-flutter.git" source: git version: "10.4.0" @@ -1943,8 +1983,8 @@ packages: dependency: "direct overridden" description: path: "packages/stream_chat_flutter" - ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" - resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" + resolved-ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" url: "https://github.com/GetStream/stream-chat-flutter.git" source: git version: "10.4.0" @@ -1952,8 +1992,8 @@ packages: dependency: "direct overridden" description: path: "packages/stream_chat_flutter_core" - ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" - resolved-ref: "3f306e5b9f8767f998387ad7b79d100ad3797098" + ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" + resolved-ref: "686a9980966872eee4b64abad2d5e8ffcdb8c29c" url: "https://github.com/GetStream/stream-chat-flutter.git" source: git version: "10.4.0" @@ -2352,6 +2392,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + windows_file_picker: + dependency: transitive + description: + name: windows_file_picker + sha256: "9f3aa833068b09e380fdc59560ad260a4a14e9397173abb533699f967443602e" + url: "https://pub.dev" + source: hosted + version: "2.0.0" x509: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b7f3ff98d..366060090 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,20 +23,9 @@ dev_dependencies: yaml: ^3.1.3 dependency_overrides: + # flutter_launcher_icons ^0.14.4 depends on cli_util ^0.4.1 + # melos >=8.7.0 depends on cli_util >=0.5.0 <0.7.0 cli_util: ^0.5.1 - file_picker: ^12.0.0-beta.5 - # A git commit rather than a release: the published 0.5.0 predates - # CurrentPlatform.debugCurrentPlatformOverride, which the tests need. - stream_core: - git: - url: https://github.com/GetStream/stream-core-flutter.git - path: packages/stream_core - ref: f4d49e9db343b48d890247fc9bf142805804daf1 - stream_core_flutter: - git: - url: https://github.com/GetStream/stream-core-flutter.git - path: packages/stream_core_flutter - ref: 2daa0a641519abe9ab637bcb41c69ed3ea4273b7 # Until a release carries the fix: the 104px avatar sizes added to # stream_core_flutter stop 10.4.0 compiling, whose size switches are # exhaustive. Only the dogfooding app pulls chat in. The chat packages share @@ -45,17 +34,29 @@ dependency_overrides: git: url: https://github.com/GetStream/stream-chat-flutter.git path: packages/stream_chat - ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 + ref: 686a9980966872eee4b64abad2d5e8ffcdb8c29c stream_chat_flutter: git: url: https://github.com/GetStream/stream-chat-flutter.git path: packages/stream_chat_flutter - ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 + ref: 686a9980966872eee4b64abad2d5e8ffcdb8c29c stream_chat_flutter_core: git: url: https://github.com/GetStream/stream-chat-flutter.git path: packages/stream_chat_flutter_core - ref: 3f306e5b9f8767f998387ad7b79d100ad3797098 + ref: 686a9980966872eee4b64abad2d5e8ffcdb8c29c + # A git commit rather than a release: the published 0.5.0 predates + # CurrentPlatform.debugCurrentPlatformOverride, which the tests need. + stream_core: + git: + url: https://github.com/GetStream/stream-core-flutter.git + path: packages/stream_core + ref: f4d49e9db343b48d890247fc9bf142805804daf1 + stream_core_flutter: + git: + url: https://github.com/GetStream/stream-core-flutter.git + path: packages/stream_core_flutter + ref: 2daa0a641519abe9ab637bcb41c69ed3ea4273b7 melos: ignore: @@ -65,8 +66,8 @@ melos: bootstrap: # Environment synced into every package during bootstrap. environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + sdk: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" # Dependency constraints synced into every package that declares them. dependencies: From 3b85a22c96d93e4f67437ed2f142f18f1cf1c1e5 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Sep 2026 11:00:52 +0200 Subject: [PATCH 10/11] feat(ui): centre the ringing screens' details above the controls The avatar, name and status sit in the middle of the space between the top of the screen and the controls, and scroll when that space is too short for them. Co-Authored-By: Claude Opus 5.5 --- .../incoming_call/incoming_call_content.dart | 60 ++++++++++--------- .../outgoing_call/outgoing_call_content.dart | 58 +++++++++--------- 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart index 7af170cd5..209b4e9e9 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/incoming_call/incoming_call_content.dart @@ -71,38 +71,42 @@ class _StreamIncomingCallContentState extends State { child: Material( color: Colors.transparent, child: SafeArea( - child: Stack( + child: Column( children: [ - Center( - child: RingingCallDetails( - participants: users, - status: context.translations.ringingIncomingCall, - style: style, - avatar: widget.participantsAvatarWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: users), - ), - nameLine: widget.participantsDisplayNameWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: users), + // The details are centred in the space above the controls, and + // scroll rather than overflow when that space is too short. + Expanded( + child: Center( + child: SingleChildScrollView( + child: RingingCallDetails( + participants: users, + status: context.translations.ringingIncomingCall, + style: style, + avatar: widget.participantsAvatarWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: users), + ), + nameLine: widget.participantsDisplayNameWidgetBuilder + ?.call( + context, + widget.call, + ParticipantsData(participants: users), + ), + ), ), ), ), - Align( - alignment: AlignmentDirectional.bottomCenter, - child: Padding( - padding: style.controlsPadding, - child: IncomingCallControls( - style: style, - isMicrophoneEnabled: connectOptions.microphone.isEnabled, - isCameraEnabled: connectOptions.camera.isEnabled, - onAcceptCallTap: _onAcceptCallTap, - onDeclineCallTap: () => _onDeclineCallTap(context), - onMicrophoneTap: () => _onMicrophoneTap(context), - onCameraTap: () => _onCameraTap(context), - ), + Padding( + padding: style.controlsPadding, + child: IncomingCallControls( + style: style, + isMicrophoneEnabled: connectOptions.microphone.isEnabled, + isCameraEnabled: connectOptions.camera.isEnabled, + onAcceptCallTap: _onAcceptCallTap, + onDeclineCallTap: () => _onDeclineCallTap(context), + onMicrophoneTap: () => _onMicrophoneTap(context), + onCameraTap: () => _onCameraTap(context), ), ), ], diff --git a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart index 82a776dfb..215b47f0a 100644 --- a/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart +++ b/packages/stream_video_flutter/lib/src/call_screen/outgoing_call/outgoing_call_content.dart @@ -126,37 +126,41 @@ class _StreamOutgoingCallContentState extends State { final child = Material( color: Colors.transparent, child: SafeArea( - child: Stack( + child: Column( children: [ - Center( - child: RingingCallDetails( - participants: participants, - status: context.translations.ringingCalling, - style: style, - avatar: widget.participantsAvatarWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: participants), - ), - nameLine: widget.participantsDisplayNameWidgetBuilder?.call( - context, - widget.call, - ParticipantsData(participants: participants), + // The details are centred in the space above the controls, and + // scroll rather than overflow when that space is too short. + Expanded( + child: Center( + child: SingleChildScrollView( + child: RingingCallDetails( + participants: participants, + status: context.translations.ringingCalling, + style: style, + avatar: widget.participantsAvatarWidgetBuilder?.call( + context, + widget.call, + ParticipantsData(participants: participants), + ), + nameLine: widget.participantsDisplayNameWidgetBuilder + ?.call( + context, + widget.call, + ParticipantsData(participants: participants), + ), + ), ), ), ), - Align( - alignment: AlignmentDirectional.bottomCenter, - child: Padding( - padding: style.controlsPadding, - child: OutgoingCallControls( - style: style, - isMicrophoneEnabled: _controller.microphoneEnabled, - isCameraEnabled: _controller.cameraEnabled, - onCancelCallTap: () => _onCancelCallTap(context), - onMicrophoneTap: _onMicrophoneTap, - onCameraTap: _onCameraTap, - ), + Padding( + padding: style.controlsPadding, + child: OutgoingCallControls( + style: style, + isMicrophoneEnabled: _controller.microphoneEnabled, + isCameraEnabled: _controller.cameraEnabled, + onCancelCallTap: () => _onCancelCallTap(context), + onMicrophoneTap: _onMicrophoneTap, + onCameraTap: _onCameraTap, ), ), ], From 636eb66ed53486cf278ca9b6a0c8b18910f01879 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:20:55 +0000 Subject: [PATCH 11/11] chore: update goldens --- .../goldens/ci/ringing_call_dark.png | Bin 23890 -> 25490 bytes .../goldens/ci/ringing_call_light.png | Bin 29429 -> 29880 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_dark.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_dark.png index 5b18b76b2e582e1fd7a3061669e21fe8bc52cdc4..8a7c157fffdf9bec88392051c71c2cce88e43c91 100644 GIT binary patch literal 25490 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 diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_light.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/ringing_call_light.png index c20d660ccbeb83e3b5f74b20b1cf4823d3eda576..8856f8a2b3666bcb37c6c69bd0100553d19c2c79 100644 GIT binary patch literal 29880 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 xRqoN4U#OEHLqe+-Qc>lZO+OJHhKh|8WUQ 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