diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index eb79620203..0f902b96fc 100644 Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 0000000000..074b231add --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/format_quote.svg b/assets/icons/format_quote.svg new file mode 100644 index 0000000000..0de3a768ce --- /dev/null +++ b/assets/icons/format_quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/share.svg b/assets/icons/share.svg new file mode 100644 index 0000000000..5031943a1c --- /dev/null +++ b/assets/icons/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/share_ios.svg b/assets/icons/share_ios.svg new file mode 100644 index 0000000000..45cf0c81dd --- /dev/null +++ b/assets/icons/share_ios.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/smile.svg b/assets/icons/smile.svg new file mode 100644 index 0000000000..2f9c5b5e74 --- /dev/null +++ b/assets/icons/smile.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000000..9c5e4069d3 --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index 8442160d40..d06a57995f 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; @@ -14,10 +15,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. /// @@ -43,21 +46,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(), + ]))); }); } @@ -77,11 +107,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))), + ); } } @@ -94,7 +160,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) { @@ -135,11 +201,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; } @@ -231,7 +299,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) { @@ -316,7 +384,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) { @@ -384,7 +452,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/icons.dart b/lib/widgets/icons.dart index 23080a2257..ebdb6a362c 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -42,38 +42,56 @@ abstract final class ZulipIcons { /// The Zulip custom icon "clock". static const IconData clock = IconData(0xf106, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "copy". + static const IconData copy = IconData(0xf107, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "format_quote". + static const IconData format_quote = IconData(0xf108, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "globe". - static const IconData globe = IconData(0xf107, fontFamily: "Zulip Icons"); + static const IconData globe = IconData(0xf109, fontFamily: "Zulip Icons"); /// The Zulip custom icon "group_dm". - static const IconData group_dm = IconData(0xf108, fontFamily: "Zulip Icons"); + static const IconData group_dm = IconData(0xf10a, fontFamily: "Zulip Icons"); /// The Zulip custom icon "hash_sign". - static const IconData hash_sign = IconData(0xf109, fontFamily: "Zulip Icons"); + static const IconData hash_sign = IconData(0xf10b, fontFamily: "Zulip Icons"); /// The Zulip custom icon "language". - static const IconData language = IconData(0xf10a, fontFamily: "Zulip Icons"); + static const IconData language = IconData(0xf10c, fontFamily: "Zulip Icons"); /// The Zulip custom icon "lock". - static const IconData lock = IconData(0xf10b, fontFamily: "Zulip Icons"); + static const IconData lock = IconData(0xf10d, fontFamily: "Zulip Icons"); /// The Zulip custom icon "mute". - static const IconData mute = IconData(0xf10c, fontFamily: "Zulip Icons"); + static const IconData mute = IconData(0xf10e, fontFamily: "Zulip Icons"); /// The Zulip custom icon "read_receipts". - static const IconData read_receipts = IconData(0xf10d, fontFamily: "Zulip Icons"); + static const IconData read_receipts = IconData(0xf10f, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "share". + static const IconData share = IconData(0xf110, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "share_ios". + static const IconData share_ios = IconData(0xf111, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "smile". + static const IconData smile = IconData(0xf112, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "star". + static const IconData star = IconData(0xf113, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star_filled". - static const IconData star_filled = IconData(0xf10e, fontFamily: "Zulip Icons"); + static const IconData star_filled = IconData(0xf114, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topic". - static const IconData topic = IconData(0xf10f, fontFamily: "Zulip Icons"); + static const IconData topic = IconData(0xf115, fontFamily: "Zulip Icons"); /// The Zulip custom icon "unmute". - static const IconData unmute = IconData(0xf110, fontFamily: "Zulip Icons"); + static const IconData unmute = IconData(0xf116, fontFamily: "Zulip Icons"); /// The Zulip custom icon "user". - static const IconData user = IconData(0xf111, fontFamily: "Zulip Icons"); + static const IconData user = IconData(0xf117, fontFamily: "Zulip Icons"); // END GENERATED ICON DATA } diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index 9f1c732f5c..0fe1ac6cfd 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), mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(), sectionCollapseIcon: const Color(0x7f1e2e48), star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), @@ -137,9 +150,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(), @@ -147,6 +164,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(), @@ -159,6 +177,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) mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(), // TODO(design-dark) need proper dark-theme color (this is ad hoc) @@ -174,9 +193,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, @@ -185,6 +208,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, @@ -193,6 +217,7 @@ class DesignVariables extends ThemeExtension { required this.groupDmConversationIconBg, required this.loginOrDivider, required this.loginOrDividerText, + required this.modalBarrierColor, required this.mutedUnreadBadge, required this.sectionCollapseIcon, required this.star, @@ -212,9 +237,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; @@ -227,6 +256,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; @@ -235,6 +265,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 mutedUnreadBadge; final Color sectionCollapseIcon; final Color star; @@ -245,9 +276,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, @@ -256,6 +291,7 @@ class DesignVariables extends ThemeExtension { Color? title, ChannelColorSwatches? channelColorSwatches, Color? atMentionMarker, + Color? contextMenuCancelBg, Color? dmHeaderBg, Color? errorBannerBackground, Color? errorBannerBorder, @@ -264,6 +300,7 @@ class DesignVariables extends ThemeExtension { Color? groupDmConversationIconBg, Color? loginOrDivider, Color? loginOrDividerText, + Color? modalBarrierColor, Color? mutedUnreadBadge, Color? sectionCollapseIcon, Color? star, @@ -273,9 +310,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, @@ -284,6 +325,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, @@ -292,6 +334,7 @@ class DesignVariables extends ThemeExtension { groupDmConversationIconBg: groupDmConversationIconBg ?? this.groupDmConversationIconBg, loginOrDivider: loginOrDivider ?? this.loginOrDivider, loginOrDividerText: loginOrDividerText ?? this.loginOrDividerText, + modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor, mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge, sectionCollapseIcon: sectionCollapseIcon ?? this.sectionCollapseIcon, star: star ?? this.star, @@ -308,9 +351,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)!, @@ -319,6 +366,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)!, @@ -327,6 +375,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)!, mutedUnreadBadge: Color.lerp(mutedUnreadBadge, other.mutedUnreadBadge, t)!, sectionCollapseIcon: Color.lerp(sectionCollapseIcon, other.sectionCollapseIcon, t)!, star: Color.lerp(star, other.star, t)!, diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index b37ca66bc5..42fca2edae 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; + + void checkActionSheet(WidgetTester tester, {required bool isShown}) { + check(find.text(zulipLocalizations.actionSheetOptionStarMessage) + .evaluate().length).equals(isShown ? 1 : 0); + + final findCancelButton = find.text(zulipLocalizations.dialogCancel); + 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); + + final findCancelButton = find.text(zulipLocalizations.dialogCancel); + await tester.tap(findCancelButton); + await tester.pumpAndSettle(); + checkActionSheet(tester, isShown: false); + }); + }); }