diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 71880b1dc2..4e858c62fe 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -1724,14 +1724,20 @@ class EditMessageComposeBoxController extends ComposeBoxController { /// A banner to display over or instead of interactive compose-box content. /// /// Must have a [PageRoot] ancestor. -abstract class _Banner extends StatelessWidget { - const _Banner(); +class _Banner extends StatelessWidget { + const _Banner({ + required this.intent, + required this.label, + this.trailing, + this.padEnd = true, // ignore: unused_element_parameter + }); - String getLabel(ZulipLocalizations zulipLocalizations); - Color getLabelColor(DesignVariables designVariables); - Color getBackgroundColor(DesignVariables designVariables); + final _BannerIntent intent; + final String label; - /// A trailing element, with vertical but not horizontal outer padding + /// An optional trailing element. + /// + /// It should include vertical but not horizontal outer padding /// for spacing/positioning. /// /// An interactive element's touchable area should have height at least 44px, @@ -1739,34 +1745,41 @@ abstract class _Banner extends StatelessWidget { /// what gets painted: /// https://github.com/zulip/zulip-flutter/pull/1432#discussion_r2023907300 /// - /// To control the element's distance from the end edge, override [padEnd]. - /// - /// The passed [BuildContext] will be the result of [PageRoot.contextOf], - /// so it's expected to remain mounted until the whole page disappears, - /// which may be long after the banner disappears. - Widget? buildTrailing(BuildContext pageContext); + /// To control the element's distance from the end edge, use [padEnd]. + // An "x" button could go here. + // 24px square with 8px touchable padding in all directions? + // and `padEnd: false`; see Figma: + // https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4031-17029&m=dev + final Widget? trailing; /// Whether to apply `end: 8` in [SafeArea.minimum]. /// - /// Subclasses can use `false` when the [buildTrailing] element + /// Pass `false` when the [trailing] element /// is meant to abut the edge of the screen /// in the common case that there are no horizontal device insets. - bool get padEnd => true; + /// + /// Defaults to `true`. + final bool padEnd; @override Widget build(BuildContext context) { - final zulipLocalizations = ZulipLocalizations.of(context); final designVariables = DesignVariables.of(context); + + final (labelColor, backgroundColor) = switch (intent) { + _BannerIntent.info => + (designVariables.bannerTextIntInfo, designVariables.bannerBgIntInfo), + _BannerIntent.danger => + (designVariables.btnLabelAttMediumIntDanger, designVariables.bannerBgIntDanger), + }; + final labelTextStyle = TextStyle( fontSize: 17, height: 22 / 17, - color: getLabelColor(designVariables), + color: labelColor, ).merge(weightVariableTextStyle(context, wght: 600)); - final trailing = buildTrailing(PageRoot.contextOf(context)); return DecoratedBox( - decoration: BoxDecoration( - color: getBackgroundColor(designVariables)), + decoration: BoxDecoration(color: backgroundColor), child: SafeArea( minimum: EdgeInsetsDirectional.only(start: 8, end: padEnd ? 8 : 0) // (SafeArea.minimum doesn't take an EdgeInsetsDirectional) @@ -1781,61 +1794,30 @@ abstract class _Banner extends StatelessWidget { child: Text( style: labelTextStyle, textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5), - getLabel(zulipLocalizations)))), + label))), if (trailing != null) ...[ const SizedBox(width: 8), - trailing, + trailing!, ], ])))); } } -class _ErrorBanner extends _Banner { - const _ErrorBanner({ - required String Function(ZulipLocalizations) getLabel, - }) : _getLabel = getLabel; - - @override - String getLabel(ZulipLocalizations zulipLocalizations) => - _getLabel(zulipLocalizations); - final String Function(ZulipLocalizations) _getLabel; - - @override - Color getLabelColor(DesignVariables designVariables) => - designVariables.btnLabelAttMediumIntDanger; - - @override - Color getBackgroundColor(DesignVariables designVariables) => - designVariables.bannerBgIntDanger; - - @override - Widget? buildTrailing(pageContext) { - // An "x" button can go here. - // 24px square with 8px touchable padding in all directions? - // and `bool get padEnd => false`; see Figma: - // https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4031-17029&m=dev - return null; - } +enum _BannerIntent { + info, + danger, } -class _EditMessageBanner extends _Banner { - const _EditMessageBanner({required this.composeBoxState}); +class _EditMessageBannerTrailing extends StatelessWidget { + const _EditMessageBannerTrailing({required this.composeBoxState}); final ComposeBoxState composeBoxState; - @override - String getLabel(ZulipLocalizations zulipLocalizations) => - zulipLocalizations.composeBoxBannerLabelEditMessage; - - @override - Color getLabelColor(DesignVariables designVariables) => - designVariables.bannerTextIntInfo; + void _handleTapSave (BuildContext context) async { + // (A BuildContext that's expected to remain mounted until the whole page + // disappears, which may be long after the banner disappears.) + final pageContext = PageRoot.contextOf(context); - @override - Color getBackgroundColor(DesignVariables designVariables) => - designVariables.bannerBgIntInfo; - - void _handleTapSave (BuildContext pageContext) async { final store = PerAccountStoreWidget.of(pageContext); final controller = composeBoxState.controller; if (controller is! EditMessageComposeBoxController) return; // TODO(log) @@ -1882,8 +1864,8 @@ class _EditMessageBanner extends _Banner { } @override - Widget buildTrailing(pageContext) { - final zulipLocalizations = ZulipLocalizations.of(pageContext); + Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); return Row(mainAxisSize: MainAxisSize.min, spacing: 8, children: [ ZulipWebUiKitButton(label: zulipLocalizations.composeBoxBannerButtonCancel, onPressed: composeBoxState.endEditInteraction), @@ -1891,7 +1873,7 @@ class _EditMessageBanner extends _Banner { // or the original raw content hasn't loaded yet ZulipWebUiKitButton(label: zulipLocalizations.composeBoxBannerButtonSave, attention: ZulipWebUiKitButtonAttention.high, - onPressed: () => _handleTapSave(pageContext)), + onPressed: () => _handleTapSave(context)), ]); } } @@ -2146,25 +2128,28 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM super.dispose(); } - /// An [_ErrorBanner] that replaces the compose box's text inputs. - Widget? _errorBannerComposingNotAllowed(BuildContext context) { + /// A [_Banner] that replaces the compose box's text inputs. + Widget? _bannerComposingNotAllowed(BuildContext context) { final store = PerAccountStoreWidget.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); switch (widget.narrow) { case ChannelNarrow(:final streamId): case TopicNarrow(:final streamId): final channel = store.streams[streamId]; if (channel == null || !store.selfCanSendMessage(inChannel: channel, byDate: DateTime.now())) { - return _ErrorBanner(getLabel: (zulipLocalizations) => - zulipLocalizations.errorBannerCannotPostInChannelLabel); + return _Banner( + intent: _BannerIntent.info, + label: zulipLocalizations.errorBannerCannotPostInChannelLabel); } case DmNarrow(:final otherRecipientIds): final hasDeactivatedUser = otherRecipientIds.any((id) => !(store.getUser(id)?.isActive ?? true)); if (hasDeactivatedUser) { - return _ErrorBanner(getLabel: (zulipLocalizations) => - zulipLocalizations.errorBannerDeactivatedDmLabel); + return _Banner( + intent: _BannerIntent.info, + label: zulipLocalizations.errorBannerDeactivatedDmLabel); } case CombinedFeedNarrow(): @@ -2178,10 +2163,12 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM @override Widget build(BuildContext context) { - final errorBanner = _errorBannerComposingNotAllowed(context); - if (errorBanner != null) { + final zulipLocalizations = ZulipLocalizations.of(context); + + final bannerComposingNotAllowed = _bannerComposingNotAllowed(context); + if (bannerComposingNotAllowed != null) { return ComposeBoxInheritedWidget.fromComposeBoxState(this, - child: _ComposeBoxContainer(body: null, banner: errorBanner)); + child: _ComposeBoxContainer(body: null, banner: bannerComposingNotAllowed)); } final Widget? body; @@ -2200,7 +2187,10 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM } case EditMessageComposeBoxController(): { body = _EditMessageComposeBoxBody(controller: controller, narrow: narrow); - banner = _EditMessageBanner(composeBoxState: this); + banner = _Banner( + intent: _BannerIntent.info, + label: zulipLocalizations.composeBoxBannerLabelEditMessage, + trailing: _EditMessageBannerTrailing(composeBoxState: this)); } }