From 92a6ae8d8211175a40835383d3c121ae7bb01db3 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Fri, 9 Aug 2024 21:58:42 +0430 Subject: [PATCH] action_sheet: Redesign bottom sheet Also, remove the custom modal bottom sheet we no longer use. Fixes: #90 --- lib/widgets/action_sheet.dart | 115 +++++++++++++---- ...aggable_scrollable_modal_bottom_sheet.dart | 118 ------------------ lib/widgets/theme.dart | 49 ++++++++ test/widgets/action_sheet_test.dart | 49 ++++++-- 4 files changed, 178 insertions(+), 153 deletions(-) delete mode 100644 lib/widgets/draggable_scrollable_modal_bottom_sheet.dart diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index 91ea939419..144ee86771 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; @@ -12,10 +13,12 @@ import 'actions.dart'; import 'clipboard.dart'; import 'compose_box.dart'; import 'dialog.dart'; -import 'draggable_scrollable_modal_bottom_sheet.dart'; import 'icons.dart'; +import 'inset_shadow.dart'; import 'message_list.dart'; import 'store.dart'; +import 'text.dart'; +import 'theme.dart'; /// Show a sheet of actions you can take on a message in the message list. /// @@ -41,21 +44,48 @@ void showMessageActionSheet({required BuildContext context, required Message mes && reactionWithVotes.userIds.contains(store.selfUserId)) ?? false; - showDraggableScrollableModalBottomSheet( + final optionButtons = [ + if (!hasThumbsUpReactionVote) + AddThumbsUpButton(message: message, messageListContext: context), + StarButton(message: message, messageListContext: context), + if (isComposeBoxOffered) + QuoteAndReplyButton(message: message, messageListContext: context), + if (showMarkAsUnreadButton) + MarkAsUnreadButton(message: message, messageListContext: context, narrow: narrow), + CopyMessageTextButton(message: message, messageListContext: context), + CopyMessageLinkButton(message: message, messageListContext: context), + ShareButton(message: message, messageListContext: context), + ]; + + showModalBottomSheet( context: context, + // Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect + // on my iPhone 13 Pro but is marked as "much slower": + // https://api.flutter.dev/flutter/dart-ui/Clip.html + clipBehavior: Clip.antiAlias, + useSafeArea: true, + isScrollControlled: true, builder: (BuildContext _) { - return Column(children: [ - if (!hasThumbsUpReactionVote) - AddThumbsUpButton(message: message, messageListContext: context), - StarButton(message: message, messageListContext: context), - if (isComposeBoxOffered) - QuoteAndReplyButton(message: message, messageListContext: context), - if (showMarkAsUnreadButton) - MarkAsUnreadButton(message: message, messageListContext: context, narrow: narrow), - CopyMessageTextButton(message: message, messageListContext: context), - CopyMessageLinkButton(message: message, messageListContext: context), - ShareButton(message: message, messageListContext: context), - ]); + return SafeArea( + minimum: const EdgeInsets.only(bottom: 16), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + // TODO(#217): show message text + Flexible(child: InsetShadowBox( + top: 8, bottom: 8, + color: DesignVariables.of(context).bgContextMenu, + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 16, bottom: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Column(spacing: 1, children: optionButtons), + )))), + const MessageActionSheetCancelButton(), + ]))); }); } @@ -75,11 +105,47 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget { @override Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); final zulipLocalizations = ZulipLocalizations.of(context); return MenuItemButton( - leadingIcon: Icon(icon), + trailingIcon: Icon(icon, color: designVariables.contextMenuItemText), + style: MenuItemButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + foregroundColor: designVariables.contextMenuItemText, + splashFactory: NoSplash.splashFactory, + ).copyWith(backgroundColor: WidgetStateColor.resolveWith((states) => + designVariables.contextMenuItemBg.withValues( + alpha: states.contains(WidgetState.pressed) ? 0.20 : 0.12))), onPressed: () => onPressed(context), - child: Text(label(zulipLocalizations))); + child: Text(label(zulipLocalizations), + style: const TextStyle(fontSize: 20, height: 24 / 20) + .merge(weightVariableTextStyle(context, wght: 600)), + )); + } +} + +class MessageActionSheetCancelButton extends StatelessWidget { + const MessageActionSheetCancelButton({super.key}); + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + return TextButton( + style: TextButton.styleFrom( + padding: const EdgeInsets.all(10), + foregroundColor: designVariables.contextMenuCancelText, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), + splashFactory: NoSplash.splashFactory, + ).copyWith(backgroundColor: WidgetStateColor.resolveWith((states) => + designVariables.contextMenuCancelBg.withValues( + alpha: states.contains(WidgetState.pressed) ? 0.20 : 0.15))), + onPressed: () { + Navigator.pop(context); + }, + child: Text(ZulipLocalizations.of(context).dialogCancel, + style: const TextStyle(fontSize: 20, height: 24 / 20) + .merge(weightVariableTextStyle(context, wght: 600))), + ); } } @@ -92,7 +158,7 @@ class AddThumbsUpButton extends MessageActionSheetMenuItemButton { required super.messageListContext, }); - @override IconData get icon => Icons.add_reaction_outlined; + @override IconData get icon => ZulipIcons.smile; @override String label(ZulipLocalizations zulipLocalizations) { @@ -133,11 +199,13 @@ class StarButton extends MessageActionSheetMenuItemButton { required super.messageListContext, }); - @override IconData get icon => ZulipIcons.star_filled; + @override IconData get icon => _isStarred ? ZulipIcons.star_filled : ZulipIcons.star; + + bool get _isStarred => message.flags.contains(MessageFlag.starred); @override String label(ZulipLocalizations zulipLocalizations) { - return message.flags.contains(MessageFlag.starred) + return _isStarred ? zulipLocalizations.actionSheetOptionUnstarMessage : zulipLocalizations.actionSheetOptionStarMessage; } @@ -229,7 +297,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton { required super.messageListContext, }); - @override IconData get icon => Icons.format_quote_outlined; + @override IconData get icon => ZulipIcons.format_quote; @override String label(ZulipLocalizations zulipLocalizations) { @@ -314,7 +382,7 @@ class CopyMessageTextButton extends MessageActionSheetMenuItemButton { required super.messageListContext, }); - @override IconData get icon => Icons.copy; + @override IconData get icon => ZulipIcons.copy; @override String label(ZulipLocalizations zulipLocalizations) { @@ -382,7 +450,10 @@ class ShareButton extends MessageActionSheetMenuItemButton { required super.messageListContext, }); - @override IconData get icon => Icons.adaptive.share; + @override + IconData get icon => defaultTargetPlatform == TargetPlatform.iOS + ? ZulipIcons.share_ios + : ZulipIcons.share; @override String label(ZulipLocalizations zulipLocalizations) { diff --git a/lib/widgets/draggable_scrollable_modal_bottom_sheet.dart b/lib/widgets/draggable_scrollable_modal_bottom_sheet.dart deleted file mode 100644 index 27f8bfd654..0000000000 --- a/lib/widgets/draggable_scrollable_modal_bottom_sheet.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter/material.dart'; - -class _DraggableScrollableLayer extends StatelessWidget { - const _DraggableScrollableLayer({required this.builder}); - - final WidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return DraggableScrollableSheet( - // Match `initial…` to `min…` so that a slight drag downward dismisses - // the sheet instead of just resizing it. Making them equal gives a - // buggy experience for some reason - // ( https://github.com/zulip/zulip-flutter/pull/12#discussion_r1116423455 ) - // so we work around by make `initial…` a bit bigger. - minChildSize: 0.25, - initialChildSize: 0.26, - - // With `expand: true`, the bottom sheet would then start out occupying - // the whole screen, as if `initialChildSize` was 1.0. That doesn't seem - // like what the docs call for. Maybe a bug. Or maybe it's somehow - // related to the `Stack`? - expand: false, - - builder: (BuildContext context, ScrollController scrollController) { - return SingleChildScrollView( - // Prevent overscroll animation on swipe down; it looks - // sloppy when you're swiping to dismiss the sheet. - physics: const ClampingScrollPhysics(), - - controller: scrollController, - - child: Padding( - // Avoid the drag handle. See comment on - // _DragHandleLayer's SizedBox.height. - padding: const EdgeInsets.only(top: kMinInteractiveDimension), - - // Extend DraggableScrollableSheet to full width so the whole - // sheet responds to drag/scroll uniformly. - child: FractionallySizedBox( - widthFactor: 1.0, - child: Builder(builder: builder)))); - }); - } -} - -class _DragHandleLayer extends StatelessWidget { - @override - Widget build(BuildContext context) { - ColorScheme colorScheme = Theme.of(context).colorScheme; - return SizedBox( - // In the spec, this is expressed as 22 logical pixels of top/bottom - // padding on the drag handle: - // https://m3.material.io/components/bottom-sheets/specs#e69f3dfb-e443-46ba-b4a8-aabc718cf335 - // The drag handle is specified with height 4 logical pixels, so we can - // get the same result by vertically centering the handle in a box with - // height 22 + 4 + 22 = 48. We have another way to say 48 -- - // kMinInteractiveDimension -- which is actually not a bad way to - // express it, since the feature was announced as "an optional drag - // handle with an accessible 48dp hit target": - // https://m3.material.io/components/bottom-sheets/overview#2cce5bae-eb83-40b0-8e52-5d0cfaa9b795 - // As a bonus, that constant is easy to use at the other layer in the - // Stack where we set the starting position of the sheet's content to - // avoid the drag handle. - height: kMinInteractiveDimension, - - child: Center( - child: ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all(Radius.circular(2)), - child: SizedBox( - // height / width / color (including opacity) from this table: - // https://m3.material.io/components/bottom-sheets/specs#7c093473-d9e1-48f3-9659-b75519c2a29d - height: 4, - width: 32, - child: ColoredBox(color: colorScheme.onSurfaceVariant.withValues(alpha: 0.40)))))); - } -} - -/// Show a modal bottom sheet that drags and scrolls to present lots of content. -/// -/// Aims to follow Material 3's "bottom sheet" with a drag handle: -/// https://m3.material.io/components/bottom-sheets/overview -Future showDraggableScrollableModalBottomSheet({ - required BuildContext context, - required WidgetBuilder builder, -}) { - return showModalBottomSheet( - context: context, - - // Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect - // on my iPhone 13 Pro but is marked as "much slower": - // https://api.flutter.dev/flutter/dart-ui/Clip.html - clipBehavior: Clip.antiAlias, - - // The spec: - // https://m3.material.io/components/bottom-sheets/specs - // defines the container's shape with the design token - // `md.sys.shape.corner.extra-large.top`, which in the table at - // https://m3.material.io/styles/shape/shape-scale-tokens#6f668ba1-b671-4ea2-bcf3-c1cff4f4099e - // maps to: - // 28dp,28dp,0dp,0dp - // SHAPE_FAMILY_ROUNDED_CORNERS - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))), - - useSafeArea: true, - isScrollControlled: true, - builder: (BuildContext context) { - // Make the content start below the drag handle in the y-direction, but - // when the content is scrollable, let it scroll under the drag handle in - // the z-direction. - return Stack( - children: [ - _DraggableScrollableLayer(builder: builder), - _DragHandleLayer(), - ]); - }); -} diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index d7a6ddabf6..a4b5ca02e7 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -88,6 +88,13 @@ ThemeData zulipThemeData(BuildContext context) { ), scaffoldBackgroundColor: designVariables.mainBackground, tooltipTheme: const TooltipThemeData(preferBelow: false), + bottomSheetTheme: BottomSheetThemeData( + clipBehavior: Clip.antiAlias, + backgroundColor: designVariables.bgContextMenu, + modalBarrierColor: designVariables.modalBarrierColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))), + ), ); } @@ -107,9 +114,13 @@ class DesignVariables extends ThemeExtension { DesignVariables.light() : this._( background: const Color(0xffffffff), + bgContextMenu: const Color(0xfff2f2f2), bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15), bgTopBar: const Color(0xfff5f5f5), borderBar: const Color(0x33000000), + contextMenuCancelText: const Color(0xff222222), + contextMenuItemBg: const Color(0xff6159e1), + contextMenuItemText: const Color(0xff381da7), icon: const Color(0xff666699), labelCounterUnread: const Color(0xff222222), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), @@ -118,6 +129,7 @@ class DesignVariables extends ThemeExtension { title: const Color(0xff1a1a1a), channelColorSwatches: ChannelColorSwatches.light, atMentionMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(), + contextMenuCancelBg: const Color(0xff797986), dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(), errorBannerBackground: const HSLColor.fromAHSL(1, 4, 0.33, 0.90).toColor(), errorBannerBorder: const HSLColor.fromAHSL(0.4, 3, 0.57, 0.33).toColor(), @@ -126,6 +138,7 @@ class DesignVariables extends ThemeExtension { groupDmConversationIconBg: const Color(0x33808080), loginOrDivider: const Color(0xffdedede), loginOrDividerText: const Color(0xff575757), + modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3), sectionCollapseIcon: const Color(0x7f1e2e48), star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(), @@ -136,9 +149,13 @@ class DesignVariables extends ThemeExtension { DesignVariables.dark() : this._( background: const Color(0xff000000), + bgContextMenu: const Color(0xff262626), bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37), bgTopBar: const Color(0xff242424), borderBar: Colors.black.withValues(alpha: 0.41), + contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75), + contextMenuItemBg: const Color(0xff7977fe), + contextMenuItemText: const Color(0xff9398fd), icon: const Color(0xff7070c2), labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(), @@ -146,6 +163,7 @@ class DesignVariables extends ThemeExtension { mainBackground: const Color(0xff1d1d1d), title: const Color(0xffffffff), channelColorSwatches: ChannelColorSwatches.dark, + contextMenuCancelBg: const Color(0xff797986), // the same as the light mode in Figma // TODO(design-dark) need proper dark-theme color (this is ad hoc) atMentionMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(), dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(), @@ -158,6 +176,7 @@ class DesignVariables extends ThemeExtension { groupDmConversationIconBg: const Color(0x33cccccc), loginOrDivider: const Color(0xff424242), loginOrDividerText: const Color(0xffa8a8a8), + modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5), // TODO(design-dark) need proper dark-theme color (this is ad hoc) sectionCollapseIcon: const Color(0x7fb6c8e2), // TODO(design-dark) unchanged in dark theme? @@ -171,9 +190,13 @@ class DesignVariables extends ThemeExtension { DesignVariables._({ required this.background, + required this.bgContextMenu, required this.bgCounterUnread, required this.bgTopBar, required this.borderBar, + required this.contextMenuCancelText, + required this.contextMenuItemBg, + required this.contextMenuItemText, required this.icon, required this.labelCounterUnread, required this.labelEdited, @@ -182,6 +205,7 @@ class DesignVariables extends ThemeExtension { required this.title, required this.channelColorSwatches, required this.atMentionMarker, + required this.contextMenuCancelBg, required this.dmHeaderBg, required this.errorBannerBackground, required this.errorBannerBorder, @@ -190,6 +214,7 @@ class DesignVariables extends ThemeExtension { required this.groupDmConversationIconBg, required this.loginOrDivider, required this.loginOrDividerText, + required this.modalBarrierColor, required this.sectionCollapseIcon, required this.star, required this.subscriptionListHeaderLine, @@ -208,9 +233,13 @@ class DesignVariables extends ThemeExtension { } final Color background; + final Color bgContextMenu; final Color bgCounterUnread; final Color bgTopBar; final Color borderBar; + final Color contextMenuCancelText; + final Color contextMenuItemBg; + final Color contextMenuItemText; final Color icon; final Color labelCounterUnread; final Color labelEdited; @@ -223,6 +252,7 @@ class DesignVariables extends ThemeExtension { // Not named variables in Figma; taken from older Figma drafts, or elsewhere. final Color atMentionMarker; + final Color contextMenuCancelBg; // In Figma, but unnamed. final Color dmHeaderBg; final Color errorBannerBackground; final Color errorBannerBorder; @@ -231,6 +261,7 @@ class DesignVariables extends ThemeExtension { final Color groupDmConversationIconBg; final Color loginOrDivider; // TODO(design-dark) need proper dark-theme color (this is ad hoc) final Color loginOrDividerText; // TODO(design-dark) need proper dark-theme color (this is ad hoc) + final Color modalBarrierColor; final Color sectionCollapseIcon; final Color star; final Color subscriptionListHeaderLine; @@ -240,9 +271,13 @@ class DesignVariables extends ThemeExtension { @override DesignVariables copyWith({ Color? background, + Color? bgContextMenu, Color? bgCounterUnread, Color? bgTopBar, Color? borderBar, + Color? contextMenuCancelText, + Color? contextMenuItemBg, + Color? contextMenuItemText, Color? icon, Color? labelCounterUnread, Color? labelEdited, @@ -251,6 +286,7 @@ class DesignVariables extends ThemeExtension { Color? title, ChannelColorSwatches? channelColorSwatches, Color? atMentionMarker, + Color? contextMenuCancelBg, Color? dmHeaderBg, Color? errorBannerBackground, Color? errorBannerBorder, @@ -259,6 +295,7 @@ class DesignVariables extends ThemeExtension { Color? groupDmConversationIconBg, Color? loginOrDivider, Color? loginOrDividerText, + Color? modalBarrierColor, Color? sectionCollapseIcon, Color? star, Color? subscriptionListHeaderLine, @@ -267,9 +304,13 @@ class DesignVariables extends ThemeExtension { }) { return DesignVariables._( background: background ?? this.background, + bgContextMenu: bgContextMenu ?? this.bgContextMenu, bgCounterUnread: bgCounterUnread ?? this.bgCounterUnread, bgTopBar: bgTopBar ?? this.bgTopBar, borderBar: borderBar ?? this.borderBar, + contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText, + contextMenuItemBg: contextMenuItemBg ?? this.contextMenuItemBg, + contextMenuItemText: contextMenuItemText ?? this.contextMenuItemBg, icon: icon ?? this.icon, labelCounterUnread: labelCounterUnread ?? this.labelCounterUnread, labelEdited: labelEdited ?? this.labelEdited, @@ -278,6 +319,7 @@ class DesignVariables extends ThemeExtension { title: title ?? this.title, channelColorSwatches: channelColorSwatches ?? this.channelColorSwatches, atMentionMarker: atMentionMarker ?? this.atMentionMarker, + contextMenuCancelBg: contextMenuCancelBg ?? this.contextMenuCancelBg, dmHeaderBg: dmHeaderBg ?? this.dmHeaderBg, errorBannerBackground: errorBannerBackground ?? this.errorBannerBackground, errorBannerBorder: errorBannerBorder ?? this.errorBannerBorder, @@ -286,6 +328,7 @@ class DesignVariables extends ThemeExtension { groupDmConversationIconBg: groupDmConversationIconBg ?? this.groupDmConversationIconBg, loginOrDivider: loginOrDivider ?? this.loginOrDivider, loginOrDividerText: loginOrDividerText ?? this.loginOrDividerText, + modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor, sectionCollapseIcon: sectionCollapseIcon ?? this.sectionCollapseIcon, star: star ?? this.star, subscriptionListHeaderLine: subscriptionListHeaderLine ?? this.subscriptionListHeaderLine, @@ -301,9 +344,13 @@ class DesignVariables extends ThemeExtension { } return DesignVariables._( background: Color.lerp(background, other.background, t)!, + bgContextMenu: Color.lerp(bgContextMenu, other.bgContextMenu, t)!, bgCounterUnread: Color.lerp(bgCounterUnread, other.bgCounterUnread, t)!, bgTopBar: Color.lerp(bgTopBar, other.bgTopBar, t)!, borderBar: Color.lerp(borderBar, other.borderBar, t)!, + contextMenuCancelText: Color.lerp(contextMenuCancelText, other.contextMenuCancelText, t)!, + contextMenuItemBg: Color.lerp(contextMenuItemBg, other.contextMenuItemBg, t)!, + contextMenuItemText: Color.lerp(contextMenuItemText, other.contextMenuItemBg, t)!, icon: Color.lerp(icon, other.icon, t)!, labelCounterUnread: Color.lerp(labelCounterUnread, other.labelCounterUnread, t)!, labelEdited: Color.lerp(labelEdited, other.labelEdited, t)!, @@ -312,6 +359,7 @@ class DesignVariables extends ThemeExtension { title: Color.lerp(title, other.title, t)!, channelColorSwatches: ChannelColorSwatches.lerp(channelColorSwatches, other.channelColorSwatches, t), atMentionMarker: Color.lerp(atMentionMarker, other.atMentionMarker, t)!, + contextMenuCancelBg: Color.lerp(contextMenuCancelBg, other.contextMenuCancelBg, t)!, dmHeaderBg: Color.lerp(dmHeaderBg, other.dmHeaderBg, t)!, errorBannerBackground: Color.lerp(errorBannerBackground, other.errorBannerBackground, t)!, errorBannerBorder: Color.lerp(errorBannerBorder, other.errorBannerBorder, t)!, @@ -320,6 +368,7 @@ class DesignVariables extends ThemeExtension { groupDmConversationIconBg: Color.lerp(groupDmConversationIconBg, other.groupDmConversationIconBg, t)!, loginOrDivider: Color.lerp(loginOrDivider, other.loginOrDivider, t)!, loginOrDividerText: Color.lerp(loginOrDividerText, other.loginOrDividerText, t)!, + modalBarrierColor: Color.lerp(modalBarrierColor, other.modalBarrierColor, t)!, sectionCollapseIcon: Color.lerp(sectionCollapseIcon, other.sectionCollapseIcon, t)!, star: Color.lerp(star, other.star, t)!, subscriptionListHeaderLine: Color.lerp(subscriptionListHeaderLine, other.subscriptionListHeaderLine, t)!, diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index b37ca66bc5..86c269b2f1 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -100,8 +100,8 @@ void main() { group('AddThumbsUpButton', () { Future tapButton(WidgetTester tester) async { - await tester.ensureVisible(find.byIcon(Icons.add_reaction_outlined, skipOffstage: false)); - await tester.tap(find.byIcon(Icons.add_reaction_outlined)); + await tester.ensureVisible(find.byIcon(ZulipIcons.smile, skipOffstage: false)); + await tester.tap(find.byIcon(ZulipIcons.smile)); await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e } @@ -147,15 +147,15 @@ void main() { }); group('StarButton', () { - Future tapButton(WidgetTester tester) async { + Future tapButton(WidgetTester tester, {bool starred = false}) async { // Starred messages include the same icon so we need to // match only by descendants of [BottomSheet]. await tester.ensureVisible(find.descendant( of: find.byType(BottomSheet), - matching: find.byIcon(ZulipIcons.star_filled, skipOffstage: false))); + matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star, skipOffstage: false))); await tester.tap(find.descendant( of: find.byType(BottomSheet), - matching: find.byIcon(ZulipIcons.star_filled))); + matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star))); await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e } @@ -186,7 +186,7 @@ void main() { final connection = store.connection as FakeApiConnection; connection.prepare(json: {}); - await tapButton(tester); + await tapButton(tester, starred: true); await tester.pump(Duration.zero); check(connection.lastRequest).isA() @@ -233,7 +233,7 @@ void main() { 'msg': 'Invalid message(s)', 'result': 'error', }); - await tapButton(tester); + await tapButton(tester, starred: true); await tester.pump(Duration.zero); // error arrives; error dialog shows await tester.tap(find.byWidget(checkErrorDialog(tester, @@ -249,14 +249,14 @@ void main() { } Widget? findQuoteAndReplyButton(WidgetTester tester) { - return tester.widgetList(find.byIcon(Icons.format_quote_outlined)).singleOrNull; + return tester.widgetList(find.byIcon(ZulipIcons.format_quote)).singleOrNull; } /// Simulates tapping the quote-and-reply button in the message action sheet. /// /// Checks that there is a quote-and-reply button. Future tapQuoteAndReplyButton(WidgetTester tester) async { - await tester.ensureVisible(find.byIcon(Icons.format_quote_outlined, skipOffstage: false)); + await tester.ensureVisible(find.byIcon(ZulipIcons.format_quote, skipOffstage: false)); final quoteAndReplyButton = findQuoteAndReplyButton(tester); check(quoteAndReplyButton).isNotNull(); await tester.tap(find.byWidget(quoteAndReplyButton!)); @@ -468,8 +468,8 @@ void main() { }); Future tapCopyMessageTextButton(WidgetTester tester) async { - await tester.ensureVisible(find.byIcon(Icons.copy, skipOffstage: false)); - await tester.tap(find.byIcon(Icons.copy)); + await tester.ensureVisible(find.byIcon(ZulipIcons.copy, skipOffstage: false)); + await tester.tap(find.byIcon(ZulipIcons.copy)); await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e } @@ -565,8 +565,8 @@ void main() { } Future tapShareButton(WidgetTester tester) async { - await tester.ensureVisible(find.byIcon(Icons.adaptive.share, skipOffstage: false)); - await tester.tap(find.byIcon(Icons.adaptive.share)); + await tester.ensureVisible(find.byIcon(ZulipIcons.share, skipOffstage: false)); + await tester.tap(find.byIcon(ZulipIcons.share)); await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e } @@ -616,4 +616,27 @@ void main() { check(mockSharePlus.sharedString).isNull(); }); }); + + group('MessageActionSheetCancelButton', () { + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + + Finder findCancelButton() => find.text(zulipLocalizations.dialogCancel); + + void checkActionSheet(WidgetTester tester, {required bool isShown}) { + check(find.text(zulipLocalizations.actionSheetOptionStarMessage) + .evaluate().length).equals(isShown ? 1 : 0); + + check(findCancelButton().evaluate().length).equals(isShown ? 1 : 0); + } + + testWidgets('pressing the button dismisses the action sheet', (tester) async { + final message = eg.streamMessage(); + await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message)); + checkActionSheet(tester, isShown: true); + + await tester.tap(findCancelButton()); + await tester.pumpAndSettle(); + checkActionSheet(tester, isShown: false); + }); + }); }