diff --git a/packages/stream_video_flutter/CHANGELOG.md b/packages/stream_video_flutter/CHANGELOG.md index 9c70a21c9..817cb0197 100644 --- a/packages/stream_video_flutter/CHANGELOG.md +++ b/packages/stream_video_flutter/CHANGELOG.md @@ -19,7 +19,7 @@ - Added `CallControlBarThemeData` on `StreamVideoTheme`, and `CallControlBarTheme` to restyle the bar over a subtree. - Added `StreamDeviceAvailability.enumerationFailed`, which tells an enumeration the platform refused from one that found no device. - Added `CallParticipantState.trackEnabled`, null for a track nothing has reported, and `TrackOption.wantsOn` for the intent a call was joined with. -- `CallButtonBadge` is no longer exported. It exists so the badge sits in the same place on both call buttons, which is an implementation detail; exporting it committed the package to its shape and gave integrators a way to badge things inconsistently. +- Added `StreamCallButtonBadge`, the badge on the call control buttons, with `StreamCallButtonBadgeTheme`, `StreamCallButtonBadgeThemeData` and `StreamCallButtonBadgeStyle` served from `StreamVideoTheme.callButtonBadgeTheme`. - `CallControlButton` no longer overrides `StreamButtonTheme` for every tone. Only `positive` repaints the primary background — there is no success button style in the design system — and wrapping the other two overrode an app's own primary style for buttons that never use it. - `StreamMenuHandle` is an `abstract interface class`, so it cannot be accidentally extended. - Added `hasNoOptions` on an `Iterable`, and `StreamAdaptiveMenuAnchor` now drops a heading with no rows under it. Every caller had to know both — "is there anything to open" was recomputed at each of two call sites, and an empty section drew a label over nothing. @@ -152,6 +152,11 @@ Every component follows the same shape: `StreamX` resolves the registered builder and falls back to `DefaultX`, which holds the default implementation. The parameters of `StreamX` are carried in a `StreamXProps`, exposed as `StreamX.props`, so a custom builder can read them and `copyWith` them to decorate the default rather than reimplement it. - Added `StreamParticipantTile`, the participant tile as a replaceable component: register a `participantTile` builder to replace it, or use `DefaultStreamParticipantTile` for the default implementation. +### 🔄 Changed + +- The badge on the call control buttons is amber with no border, where it used to be red with one. +- `accentWarning` is a lighter amber, which also repaints the fair bars on `StreamConnectionQualityIndicator`. + ### 🐞 Fixed - Fixed the participant grid rearranging itself when a participant nobody can see starts speaking. They take the place of the tile with the least claim to one — the last one on screen — instead of the first, which used to move every tile below it down one. diff --git a/packages/stream_video_flutter/lib/src/call_controls/call_button_badge.dart b/packages/stream_video_flutter/lib/src/call_controls/call_button_badge.dart deleted file mode 100644 index 5f514deed..000000000 --- a/packages/stream_video_flutter/lib/src/call_controls/call_button_badge.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../stream_video_flutter.dart'; - -/// Overlays an error badge on the top-end corner of a call button. -/// -/// Shared by [CallControlButton] and [CallFeatureButton] so the badge sits in -/// the same place on both. -class CallButtonBadge extends StatelessWidget { - /// Creates a new instance of [CallButtonBadge]. - const CallButtonBadge({ - super.key, - required this.showErrorBadge, - required this.child, - }); - - /// Whether to draw the badge at all. - final bool showErrorBadge; - - /// The button to badge. - final Widget child; - - @override - Widget build(BuildContext context) { - if (!showErrorBadge) return child; - - return Stack( - // The badge deliberately overhangs the button's box, so the stack must - // not clip it away. - clipBehavior: Clip.none, - children: [ - child, - PositionedDirectional( - top: -4, - end: -4, - child: StreamErrorBadge(size: .sm), - ), - ], - ); - } -} 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 39b6986eb..84e9d1831 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 @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../stream_video_flutter.dart'; -import 'call_button_badge.dart'; /// The tone of a [CallControlButton]. /// @@ -73,7 +72,7 @@ class CallControlButton extends StatelessWidget { @override Widget build(BuildContext context) { - final button = CallButtonBadge( + final button = StreamCallButtonBadge( showErrorBadge: showErrorBadge, child: StreamButton.icon( icon: icon, diff --git a/packages/stream_video_flutter/lib/src/call_controls/call_feature_button.dart b/packages/stream_video_flutter/lib/src/call_controls/call_feature_button.dart index cb633f0e0..a328dd7e3 100644 --- a/packages/stream_video_flutter/lib/src/call_controls/call_feature_button.dart +++ b/packages/stream_video_flutter/lib/src/call_controls/call_feature_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../stream_video_flutter.dart'; -import 'call_button_badge.dart'; /// The colour a [CallFeatureButton] takes while it is on. /// @@ -76,7 +75,7 @@ class CallFeatureButton extends StatelessWidget { @override Widget build(BuildContext context) { - return CallButtonBadge( + return StreamCallButtonBadge( showErrorBadge: showErrorBadge, child: StreamButton.icon( icon: icon, diff --git a/packages/stream_video_flutter/lib/src/call_controls/device_split_buttons.dart b/packages/stream_video_flutter/lib/src/call_controls/device_split_buttons.dart index 1957fecd9..7cde8bee6 100644 --- a/packages/stream_video_flutter/lib/src/call_controls/device_split_buttons.dart +++ b/packages/stream_video_flutter/lib/src/call_controls/device_split_buttons.dart @@ -8,7 +8,6 @@ import 'package:stream_core_flutter/video.dart'; import '../../stream_video_flutter.dart'; import '../l10n/localization_extension.dart'; -import 'call_button_badge.dart'; /// Turns the microphone on and off, with a caret that picks which microphone /// and speaker to use. @@ -408,7 +407,7 @@ class _DeviceSplitButton extends StatelessWidget { // the leading half can be pressed is the caller's to say — a failed // open is worth retrying — while the caret follows what it has to // offer, which is nothing when the platform named no device. - builder: (context, handle) => CallButtonBadge( + builder: (context, handle) => StreamCallButtonBadge( showErrorBadge: unavailable, child: StreamSplitButton.icon( leadingIcon: Icon(icon), diff --git a/packages/stream_video_flutter/lib/src/call_controls/stream_call_button_badge.dart b/packages/stream_video_flutter/lib/src/call_controls/stream_call_button_badge.dart new file mode 100644 index 000000000..3dffdb031 --- /dev/null +++ b/packages/stream_video_flutter/lib/src/call_controls/stream_call_button_badge.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import '../../stream_video_flutter.dart'; + +/// Overlays an error badge on the top-end corner of a call button. +/// +/// Shared by [CallControlButton] and [CallFeatureButton] so the badge sits in +/// the same place on both. +/// +/// Styling resolves from [StreamCallButtonBadgeTheme], falling back to the +/// design system's defaults for a call control — see +/// [StreamCallButtonBadgeStyle]. +class StreamCallButtonBadge extends StatelessWidget { + /// Creates a new instance of [StreamCallButtonBadge]. + const StreamCallButtonBadge({ + super.key, + required this.showErrorBadge, + required this.child, + this.style, + }); + + /// Whether to draw the badge at all. + final bool showErrorBadge; + + /// The button to badge. + final Widget child; + + /// Overrides for the badge's styling. + /// + /// Takes precedence over [StreamCallButtonBadgeTheme] for the properties it + /// sets; the rest still resolve from the theme. + final StreamCallButtonBadgeStyle? style; + + @override + Widget build(BuildContext context) { + if (!showErrorBadge) return child; + + const defaults = _StreamCallButtonBadgeStyleDefaults(); + final themeStyle = StreamCallButtonBadgeTheme.of(context).style; + final effective = themeStyle?.merge(style) ?? style; + + final overhang = effective?.overhang ?? defaults.overhang; + + return Stack( + // The badge deliberately overhangs the button's box, so the stack must + // not clip it away. + clipBehavior: Clip.none, + children: [ + child, + PositionedDirectional( + top: -overhang, + end: -overhang, + child: StreamErrorBadge( + size: effective?.size ?? defaults.size, + style: effective?.badgeStyle ?? defaults.badgeStyle, + showBorder: effective?.showBorder ?? defaults.showBorder, + ), + ), + ], + ); + } +} + +// Default style values for [StreamCallButtonBadge]. +// +// The badge sits on a call control, which is itself often over video, so the +// design system gives it the warning severity rather than the shared error one. +class _StreamCallButtonBadgeStyleDefaults extends StreamCallButtonBadgeStyle { + const _StreamCallButtonBadgeStyleDefaults(); + + @override + StreamErrorBadgeStyle get badgeStyle => StreamErrorBadgeStyle.warning; + + @override + StreamErrorBadgeSize get size => StreamErrorBadgeSize.sm; + + @override + bool get showBorder => false; + + @override + double get overhang => 4; +} diff --git a/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.dart b/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.dart new file mode 100644 index 000000000..1e666eed0 --- /dev/null +++ b/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.dart @@ -0,0 +1,150 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../../../stream_video_flutter.dart'; + +part 'call_button_badge_theme.g.theme.dart'; + +/// Applies a call button badge theme to descendant [StreamCallButtonBadge] +/// widgets. +/// +/// Wrap a subtree with [StreamCallButtonBadgeTheme] to override how the badge +/// on a call control reads. +/// +/// {@tool snippet} +/// +/// Make the badge read as an error rather than a warning: +/// +/// ```dart +/// StreamCallButtonBadgeTheme( +/// data: StreamCallButtonBadgeThemeData( +/// style: StreamCallButtonBadgeStyle( +/// badgeStyle: StreamErrorBadgeStyle.error, +/// ), +/// ), +/// child: child, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamCallButtonBadgeThemeData], which describes the theme. +/// * [StreamCallButtonBadgeStyle], the visual style it carries. +class StreamCallButtonBadgeTheme extends InheritedTheme { + /// Creates a call button badge theme. + const StreamCallButtonBadgeTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The badge theme data for descendant widgets. + final StreamCallButtonBadgeThemeData data; + + /// Returns the [StreamCallButtonBadgeThemeData] merged from local and global + /// themes. + /// + /// Local values from the nearest [StreamCallButtonBadgeTheme] ancestor take + /// precedence over the global values from + /// [StreamVideoTheme.callButtonBadgeTheme]. This allows partial overrides: + /// setting only [StreamCallButtonBadgeStyle.size] leaves the severity coming + /// from the global theme. + static StreamCallButtonBadgeThemeData of(BuildContext context) { + final localTheme = context + .dependOnInheritedWidgetOfExactType(); + return StreamVideoTheme.of(context).callButtonBadgeTheme.merge( + localTheme?.data, + ); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamCallButtonBadgeTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamCallButtonBadgeTheme oldWidget) => + data != oldWidget.data; +} + +/// Theme data for customizing [StreamCallButtonBadge] widgets. +/// +/// Wraps a [StreamCallButtonBadgeStyle] so it can be served by +/// [StreamCallButtonBadgeTheme] and slotted into [StreamVideoTheme] alongside +/// the other component theme data classes. +/// +/// See also: +/// +/// * [StreamCallButtonBadgeStyle], the style embedded here. +/// * [StreamCallButtonBadgeTheme], for overriding it in a subtree. +@themeGen +@immutable +class StreamCallButtonBadgeThemeData with _$StreamCallButtonBadgeThemeData { + /// Creates call button badge theme data. + const StreamCallButtonBadgeThemeData({this.style}); + + /// Visual styling for the badge. + final StreamCallButtonBadgeStyle? style; + + /// Linearly interpolate between two theme data objects. + static StreamCallButtonBadgeThemeData? lerp( + StreamCallButtonBadgeThemeData? a, + StreamCallButtonBadgeThemeData? b, + double t, + ) => _$StreamCallButtonBadgeThemeData.lerp(a, b, t); +} + +/// Visual styling properties for a [StreamCallButtonBadge]. +/// +/// The badge is a [StreamErrorBadge] pinned to the top-end corner of a call +/// control. This style selects which badge is drawn and where; its colors come +/// from [StreamErrorBadgeTheme] rather than being repeated here, so an app that +/// wants different badge colors themes [StreamErrorBadge] itself. +/// +/// Size is the exception: [StreamCallButtonBadge] always passes it, so it comes +/// from here and [StreamErrorBadgeThemeData.size] never reaches this badge. +/// [StreamErrorBadgeThemeData.border] applies only when [showBorder] is set +/// here. +@themeGen +@immutable +class StreamCallButtonBadgeStyle with _$StreamCallButtonBadgeStyle { + /// Creates a badge style with optional property overrides. + const StreamCallButtonBadgeStyle({ + this.badgeStyle, + this.size, + this.showBorder, + this.overhang, + }); + + /// The severity the badge conveys. + /// + /// If null, [StreamCallButtonBadge] uses [StreamErrorBadgeStyle.warning], + /// which the design system specifies for call controls. + final StreamErrorBadgeStyle? badgeStyle; + + /// The diameter of the badge. + /// + /// If null, [StreamCallButtonBadge] uses [StreamErrorBadgeSize.sm]. + final StreamErrorBadgeSize? size; + + /// Whether a border is drawn around the badge. + /// + /// If null, no border is drawn. Note that [StreamErrorBadge] draws one by + /// default, so this is a departure from the core badge. + final bool? showBorder; + + /// How far the badge overhangs the button's top-end corner, in logical + /// pixels. + /// + /// Applied upwards and towards the end edge, so the horizontal half follows + /// the text direction. If null, 4. + final double? overhang; + + /// Linearly interpolate between two styles. + static StreamCallButtonBadgeStyle? lerp( + StreamCallButtonBadgeStyle? a, + StreamCallButtonBadgeStyle? b, + double t, + ) => _$StreamCallButtonBadgeStyle.lerp(a, b, t); +} diff --git a/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.g.theme.dart b/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.g.theme.dart new file mode 100644 index 000000000..d99196caf --- /dev/null +++ b/packages/stream_video_flutter/lib/src/theme/components/call_button_badge_theme.g.theme.dart @@ -0,0 +1,175 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'call_button_badge_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamCallButtonBadgeThemeData { + bool get canMerge => true; + + static StreamCallButtonBadgeThemeData? lerp( + StreamCallButtonBadgeThemeData? a, + StreamCallButtonBadgeThemeData? 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 StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle.lerp(a.style, b.style, t), + ); + } + + StreamCallButtonBadgeThemeData copyWith({StreamCallButtonBadgeStyle? style}) { + final _this = (this as StreamCallButtonBadgeThemeData); + + return StreamCallButtonBadgeThemeData(style: style ?? _this.style); + } + + StreamCallButtonBadgeThemeData merge(StreamCallButtonBadgeThemeData? other) { + final _this = (this as StreamCallButtonBadgeThemeData); + + 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 StreamCallButtonBadgeThemeData); + final _other = (other as StreamCallButtonBadgeThemeData); + + return _other.style == _this.style; + } + + @override + int get hashCode { + final _this = (this as StreamCallButtonBadgeThemeData); + + return Object.hash(runtimeType, _this.style); + } +} + +mixin _$StreamCallButtonBadgeStyle { + bool get canMerge => true; + + static StreamCallButtonBadgeStyle? lerp( + StreamCallButtonBadgeStyle? a, + StreamCallButtonBadgeStyle? 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 StreamCallButtonBadgeStyle( + badgeStyle: t < 0.5 ? a.badgeStyle : b.badgeStyle, + size: t < 0.5 ? a.size : b.size, + showBorder: t < 0.5 ? a.showBorder : b.showBorder, + overhang: lerpDouble$(a.overhang, b.overhang, t), + ); + } + + StreamCallButtonBadgeStyle copyWith({ + StreamErrorBadgeStyle? badgeStyle, + StreamErrorBadgeSize? size, + bool? showBorder, + double? overhang, + }) { + final _this = (this as StreamCallButtonBadgeStyle); + + return StreamCallButtonBadgeStyle( + badgeStyle: badgeStyle ?? _this.badgeStyle, + size: size ?? _this.size, + showBorder: showBorder ?? _this.showBorder, + overhang: overhang ?? _this.overhang, + ); + } + + StreamCallButtonBadgeStyle merge(StreamCallButtonBadgeStyle? other) { + final _this = (this as StreamCallButtonBadgeStyle); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + badgeStyle: other.badgeStyle, + size: other.size, + showBorder: other.showBorder, + overhang: other.overhang, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamCallButtonBadgeStyle); + final _other = (other as StreamCallButtonBadgeStyle); + + return _other.badgeStyle == _this.badgeStyle && + _other.size == _this.size && + _other.showBorder == _this.showBorder && + _other.overhang == _this.overhang; + } + + @override + int get hashCode { + final _this = (this as StreamCallButtonBadgeStyle); + + return Object.hash( + runtimeType, + _this.badgeStyle, + _this.size, + _this.showBorder, + _this.overhang, + ); + } +} 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 2f1b1f398..93fd965ae 100644 --- a/packages/stream_video_flutter/lib/src/theme/components/components.dart +++ b/packages/stream_video_flutter/lib/src/theme/components/components.dart @@ -1,3 +1,4 @@ +export 'call_button_badge_theme.dart'; export 'call_control_bar_theme.dart'; export 'call_participants_grid_theme.dart'; export 'connection_quality_indicator_theme.dart'; 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 d14eab5d6..3154f4c60 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 @@ -42,6 +42,7 @@ class StreamVideoTheme extends ThemeExtension { StreamParticipantLabelThemeData? participantLabelTheme, StreamConnectionQualityIndicatorThemeData? connectionQualityIndicatorTheme, StreamCallParticipantsGridThemeData? callParticipantsGridTheme, + StreamCallButtonBadgeThemeData? callButtonBadgeTheme, StreamLivestreamThemeData? livestreamTheme, }) { final isDark = brightness == Brightness.dark; @@ -87,6 +88,7 @@ class StreamVideoTheme extends ThemeExtension { callParticipantsGridTheme: callParticipantsGridTheme ?? legacy?.toCallParticipantsGridThemeData(), + callButtonBadgeTheme: callButtonBadgeTheme, livestreamTheme: livestreamTheme, ); @@ -131,6 +133,7 @@ class StreamVideoTheme extends ThemeExtension { const StreamConnectionQualityIndicatorThemeData(), this.callParticipantsGridTheme = const StreamCallParticipantsGridThemeData(), + this.callButtonBadgeTheme = const StreamCallButtonBadgeThemeData(), required this.livestreamTheme, }); @@ -414,6 +417,9 @@ class StreamVideoTheme extends ThemeExtension { /// Theme for the participants grid layout. final StreamCallParticipantsGridThemeData callParticipantsGridTheme; + /// Theme for the badge overlaid on a call control button. + final StreamCallButtonBadgeThemeData callButtonBadgeTheme; + /// Theme for the outgoing call widget. final StreamLivestreamThemeData livestreamTheme; @@ -454,6 +460,7 @@ class StreamVideoTheme extends ThemeExtension { StreamParticipantLabelThemeData? participantLabelTheme, StreamConnectionQualityIndicatorThemeData? connectionQualityIndicatorTheme, StreamCallParticipantsGridThemeData? callParticipantsGridTheme, + StreamCallButtonBadgeThemeData? callButtonBadgeTheme, StreamLivestreamThemeData? livestreamTheme, }) => StreamVideoTheme.raw( textTheme: this.textTheme.merge(textTheme), @@ -482,6 +489,7 @@ class StreamVideoTheme extends ThemeExtension { callParticipantsGridTheme: this.callParticipantsGridTheme.merge( callParticipantsGridTheme, ), + callButtonBadgeTheme: this.callButtonBadgeTheme.merge(callButtonBadgeTheme), livestreamTheme: this.livestreamTheme.merge(livestreamTheme), ); @@ -517,6 +525,9 @@ class StreamVideoTheme extends ThemeExtension { callParticipantsGridTheme: callParticipantsGridTheme.merge( other.callParticipantsGridTheme, ), + callButtonBadgeTheme: callButtonBadgeTheme.merge( + other.callButtonBadgeTheme, + ), livestreamTheme: livestreamTheme.merge(other.livestreamTheme), ); } @@ -591,6 +602,13 @@ class StreamVideoTheme extends ThemeExtension { t, ) ?? callParticipantsGridTheme, + callButtonBadgeTheme: + StreamCallButtonBadgeThemeData.lerp( + callButtonBadgeTheme, + other.callButtonBadgeTheme, + t, + ) ?? + callButtonBadgeTheme, livestreamTheme: livestreamTheme.lerp(other.livestreamTheme, t), ); } diff --git a/packages/stream_video_flutter/lib/stream_video_flutter.dart b/packages/stream_video_flutter/lib/stream_video_flutter.dart index 73434db15..b2b14adba 100644 --- a/packages/stream_video_flutter/lib/stream_video_flutter.dart +++ b/packages/stream_video_flutter/lib/stream_video_flutter.dart @@ -32,6 +32,7 @@ export 'src/call_controls/controls/stream_speakerphone_button.dart'; export 'src/call_controls/device_control.dart'; export 'src/call_controls/device_split_buttons.dart'; export 'src/call_controls/participants_button.dart'; +export 'src/call_controls/stream_call_button_badge.dart'; export 'src/call_participants/call_participants.dart'; export 'src/call_participants/call_participants_sorting_mixin.dart'; export 'src/call_participants/floating_participant_tile.dart'; diff --git a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_dark.png b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_dark.png index 99f8434d4..6ef486f86 100644 Binary files a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_dark.png and b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_dark.png differ diff --git a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_light.png b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_light.png index ce4191939..28bb5e5af 100644 Binary files a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_light.png and b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_control_button_light.png differ diff --git a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_dark.png b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_dark.png index 21608d465..2c39bdfaa 100644 Binary files a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_dark.png and b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_dark.png differ diff --git a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_light.png b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_light.png index a62cc2d7a..baeff7884 100644 Binary files a/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_light.png and b/packages/stream_video_flutter/test/src/call_controls/goldens/ci/call_feature_button_light.png differ diff --git a/packages/stream_video_flutter/test/src/call_controls/stream_call_button_badge_test.dart b/packages/stream_video_flutter/test/src/call_controls/stream_call_button_badge_test.dart new file mode 100644 index 000000000..752d43d3a --- /dev/null +++ b/packages/stream_video_flutter/test/src/call_controls/stream_call_button_badge_test.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_video_flutter/stream_video_flutter.dart'; + +import '../../test_utils/test_wrapper.dart'; + +void main() { + const child = SizedBox(key: Key('anchor'), width: 40, height: 40); + + StreamErrorBadgeProps badge(WidgetTester tester) => + tester.widget(find.byType(StreamErrorBadge)).props; + + PositionedDirectional position(WidgetTester tester) => + tester.widget(find.byType(PositionedDirectional)); + + group('StreamCallButtonBadge', () { + testWidgets('draws nothing when showErrorBadge is false', (tester) async { + await tester.pumpWidget( + const TestWrapper( + child: StreamCallButtonBadge(showErrorBadge: false, child: child), + ), + ); + + // The widget itself is always in the tree — it returns the child + // unwrapped — so the badge is what has to be absent. + expect(find.byType(StreamErrorBadge), findsNothing); + expect(find.byType(Stack), findsNothing); + }); + + testWidgets('takes the design system defaults for a call control', ( + tester, + ) async { + await tester.pumpWidget( + const TestWrapper( + child: StreamCallButtonBadge(showErrorBadge: true, child: child), + ), + ); + + expect(badge(tester).style, StreamErrorBadgeStyle.warning); + expect(badge(tester).size, StreamErrorBadgeSize.sm); + // Core's badge draws a border by default; a call control's does not. + expect(badge(tester).showBorder, isFalse); + expect(position(tester).top, -4); + expect(position(tester).end, -4); + }); + + testWidgets('resolves the global theme', (tester) async { + await tester.pumpWidget( + MaterialApp( + theme: streamTestTheme().copyWith( + extensions: [ + StreamTheme.light(), + StreamVideoTheme.light().copyWith( + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle( + badgeStyle: StreamErrorBadgeStyle.error, + overhang: 8, + ), + ), + ), + ], + ), + home: const StreamCallButtonBadge( + showErrorBadge: true, + child: child, + ), + ), + ); + + expect(badge(tester).style, StreamErrorBadgeStyle.error); + expect(position(tester).top, -8); + }); + + testWidgets('merges a local override over the global theme', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: streamTestTheme().copyWith( + extensions: [ + StreamTheme.light(), + StreamVideoTheme.light().copyWith( + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle( + badgeStyle: StreamErrorBadgeStyle.error, + size: StreamErrorBadgeSize.md, + ), + ), + ), + ], + ), + home: const StreamCallButtonBadgeTheme( + data: StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle(size: StreamErrorBadgeSize.xs), + ), + child: StreamCallButtonBadge(showErrorBadge: true, child: child), + ), + ), + ); + + // The local value wins... + expect(badge(tester).size, StreamErrorBadgeSize.xs); + // ...and the global one it did not mention survives. + expect(badge(tester).style, StreamErrorBadgeStyle.error); + }); + + testWidgets('style beats the theme for the properties it sets', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: streamTestTheme().copyWith( + extensions: [ + StreamTheme.light(), + StreamVideoTheme.light().copyWith( + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle( + badgeStyle: StreamErrorBadgeStyle.error, + size: StreamErrorBadgeSize.md, + ), + ), + ), + ], + ), + home: const StreamCallButtonBadge( + showErrorBadge: true, + style: StreamCallButtonBadgeStyle(size: StreamErrorBadgeSize.xs), + child: child, + ), + ), + ); + + expect(badge(tester).size, StreamErrorBadgeSize.xs); + expect(badge(tester).style, StreamErrorBadgeStyle.error); + }); + + testWidgets('the overhang follows the text direction', (tester) async { + await tester.pumpWidget( + const TestWrapper( + child: Directionality( + textDirection: TextDirection.rtl, + child: StreamCallButtonBadge(showErrorBadge: true, child: child), + ), + ), + ); + + // `end` is the left edge under RTL, and the vertical half does not flip. + final rendered = tester.getTopLeft(find.byType(StreamErrorBadge)); + final anchor = tester.getTopLeft(find.byKey(const Key('anchor'))); + expect(rendered.dx, lessThan(anchor.dx)); + expect(rendered.dy, lessThan(anchor.dy)); + }); + }); +} diff --git a/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_dark.png b/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_dark.png index 6ebe966a8..89eaf5f21 100644 Binary files a/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_dark.png and b/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_dark.png differ diff --git a/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_light.png b/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_light.png index 942d6b735..d3cd846e7 100644 Binary files a/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_light.png and b/packages/stream_video_flutter/test/src/call_participants/goldens/ci/stream_connection_quality_indicator_light.png differ diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_dark.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_dark.png index 5a5b0e74a..bd331d0be 100644 Binary files a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_dark.png and b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_dark.png differ diff --git a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_light.png b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_light.png index 5db4e4a4e..7794cf27e 100644 Binary files a/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_light.png and b/packages/stream_video_flutter/test/src/call_screen/goldens/ci/stream_lobby_view_unavailable_light.png differ diff --git a/packages/stream_video_flutter/test/src/theme/participant_tile_theme_test.dart b/packages/stream_video_flutter/test/src/theme/participant_tile_theme_test.dart index 1ae26b233..9578adc12 100644 --- a/packages/stream_video_flutter/test/src/theme/participant_tile_theme_test.dart +++ b/packages/stream_video_flutter/test/src/theme/participant_tile_theme_test.dart @@ -96,12 +96,34 @@ void main() { const StreamFloatingParticipantTileThemeData( style: StreamFloatingParticipantTileStyle(elevation: 9), ), + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle(overhang: 6), + ), ); expect(theme.participantLabelTheme.style?.blurSigma, 4); expect(theme.connectionQualityIndicatorTheme.style?.size, 40); expect(theme.callParticipantsGridTheme.mainAxisSpacing, 2); expect(theme.floatingParticipantTileTheme.style?.elevation, 9); + expect(theme.callButtonBadgeTheme.style?.overhang, 6); + }); + + test('merge carries every component theme of the other theme', () { + final theme = StreamVideoTheme.light(); + final other = StreamVideoTheme.light().copyWith( + connectionQualityIndicatorTheme: + const StreamConnectionQualityIndicatorThemeData( + style: StreamConnectionQualityIndicatorStyle(size: 40), + ), + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle(overhang: 6), + ), + ); + + final merged = theme.merge(other); + + expect(merged.connectionQualityIndicatorTheme.style?.size, 40); + expect(merged.callButtonBadgeTheme.style?.overhang, 6); }); test('defaults every new component theme to an empty instance', () { @@ -114,6 +136,7 @@ void main() { expect(theme.connectionQualityIndicatorTheme.style, isNull); expect(theme.floatingParticipantTileTheme.style, isNull); expect(theme.callParticipantsGridTheme.padding, isNull); + expect(theme.callButtonBadgeTheme.style, isNull); }); test('lerp interpolates the new component themes', () { @@ -134,5 +157,22 @@ void main() { expect(mid.connectionQualityIndicatorTheme.style?.size, 30); }); + + test('lerp interpolates the call button badge overhang', () { + final a = StreamVideoTheme.light().copyWith( + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle(overhang: 4), + ), + ); + final b = StreamVideoTheme.light().copyWith( + callButtonBadgeTheme: const StreamCallButtonBadgeThemeData( + style: StreamCallButtonBadgeStyle(overhang: 8), + ), + ); + + final mid = a.lerp(b, 0.5) as StreamVideoTheme; + + expect(mid.callButtonBadgeTheme.style?.overhang, 6); + }); }); } diff --git a/pubspec.lock b/pubspec.lock index ae4f729a2..d739e78ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: cached_network_image_ce - sha256: "9c61372fecd5f6ac6aaa66fa6808b4c303b8574875e853a0916116bf2fae6840" + sha256: "9165bb982487e899e4b35729e821e4e2d2407fb9b0c8d4e07d3ea39e148200ba" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.12.0" cached_network_image_platform_interface: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: cached_network_image_web_ce - sha256: "6bcd8608d4ace7c35625f09d971ce68c7a6581a4631b0fb6ceb2909b2c2e17a8" + sha256: "22cdf044cd013ff8d6eb06ce148bfe3f1106388ca2af244e2358739debb4459c" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: @@ -719,10 +719,10 @@ packages: dependency: transitive description: name: flutter_markdown_plus - sha256: "3893b00caa52ce75cf9212f2c159ea517b9d64a641fc2226811bf5f218b5b77e" + sha256: fce641d6c2106cc495de1cd603f97a4f9d615a97a64011928ded380dbdade935 url: "https://pub.dev" source: hosted - version: "1.0.9" + version: "1.0.12" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1955,20 +1955,19 @@ packages: source: hosted version: "10.4.0" stream_core: - dependency: "direct overridden" + dependency: transitive description: - path: "packages/stream_core" - ref: f83b5d4d706a79fc429de2d27aead4394b83c1fb - resolved-ref: f83b5d4d706a79fc429de2d27aead4394b83c1fb - url: "https://github.com/GetStream/stream-core-flutter.git" - source: git - version: "0.4.0" + name: stream_core + sha256: "25c19466b96050354e9a64fb13767956b48b9405576472005598733502ef219c" + url: "https://pub.dev" + source: hosted + version: "0.5.0" stream_core_flutter: dependency: "direct overridden" description: path: "packages/stream_core_flutter" - ref: "3ab8dbeadc0bfe28c08a732858ec970c02e67c42" - resolved-ref: "3ab8dbeadc0bfe28c08a732858ec970c02e67c42" + ref: f4d49e9db343b48d890247fc9bf142805804daf1 + resolved-ref: f4d49e9db343b48d890247fc9bf142805804daf1 url: "https://github.com/GetStream/stream-core-flutter.git" source: git version: "0.5.1" diff --git a/pubspec.yaml b/pubspec.yaml index a47445b60..7363a7252 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,7 @@ dependency_overrides: git: url: https://github.com/GetStream/stream-core-flutter.git path: packages/stream_core_flutter - ref: 3ab8dbeadc0bfe28c08a732858ec970c02e67c42 + ref: f4d49e9db343b48d890247fc9bf142805804daf1 melos: ignore: