Skip to content
Merged
159 changes: 117 additions & 42 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ import 'text.dart';
import 'theme.dart';
import 'topic_list.dart';

/// Show an action sheet with scrollable menu buttons
/// and an optional scrollable header.
///
/// [header] should not use vertical padding to position itself on the sheet.
/// It will be wrapped in vertical padding
/// and, if [headerScrollable], a scroll view and an [InsetShadowBox].
void _showActionSheet(
BuildContext pageContext, {
Widget? header,
bool headerScrollable = true,
required List<List<Widget>> buttonSections,
}) {
assert(header is! BottomSheetHeader || !header.outerVerticalPadding);

// Could omit this if we need _showActionSheet outside a per-account context.
final accountId = PerAccountStoreWidget.accountIdOf(pageContext);

Expand All @@ -53,6 +62,52 @@ void _showActionSheet(
isScrollControlled: true,
builder: (BuildContext _) {
final designVariables = DesignVariables.of(pageContext);

Widget? effectiveHeader;
if (header != null) {
effectiveHeader = headerScrollable
? Flexible(
// TODO(upstream) Enforce a flex ratio (e.g. 1:3)
// only when the header height plus the buttons' height
// exceeds available space. Otherwise let one or the other
// grow to fill available space even if it breaks the ratio.
// Needs support for separate properties like `flex-grow`
// and `flex-shrink`.
flex: 1,
child: InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 8),
child: header)))
: Padding(
padding: EdgeInsets.only(top: 16, bottom: 4),
child: header);
}

final body = Flexible(
flex: (effectiveHeader != null && headerScrollable)
? 3
: 1,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: buttonSections.map((buttons) =>
MenuButtonsShape(buttons: buttons)).toList())))),
const BottomSheetDismissButton(style: BottomSheetDismissButtonStyle.cancel),
])));

return PerAccountStoreWidget(
accountId: accountId,
child: Semantics(
Expand All @@ -62,43 +117,11 @@ void _showActionSheet(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (header != null)
Flexible(
// TODO(upstream) Enforce a flex ratio (e.g. 1:3)
// only when the header height plus the buttons' height
// exceeds available space. Otherwise let one or the other
// grow to fill available space even if it breaks the ratio.
// Needs support for separate properties like `flex-grow`
// and `flex-shrink`.
flex: 1,
child: InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 8),
child: header)))
if (effectiveHeader != null)
effectiveHeader
else
SizedBox(height: 8),
Flexible(
flex: 3,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: buttonSections.map((buttons) =>
MenuButtonsShape(buttons: buttons)).toList())))),
const BottomSheetDismissButton(style: BottomSheetDismissButtonStyle.cancel),
]))),
body,
]))));
});
}
Expand All @@ -114,7 +137,8 @@ typedef WidgetBuilderFromTextStyle = Widget Function(TextStyle);
/// The "build" params support richer content, such as [TextWithLink],
/// and the callback is passed a [TextStyle] which is the base style.
///
/// Assumes 8px padding below the top of the bottom sheet.
/// To add outer vertical padding to position the header on the sheet,
/// pass true for [outerVerticalPadding].
///
/// Figma; just message no title:
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev
Expand All @@ -133,6 +157,7 @@ class BottomSheetHeader extends StatelessWidget {
this.buildTitle,
this.message,
this.buildMessage,
this.outerVerticalPadding = false,
}) : assert(message == null || buildMessage == null),
assert(title == null || buildTitle == null),
assert((message != null || buildMessage != null)
Expand All @@ -142,23 +167,38 @@ class BottomSheetHeader extends StatelessWidget {
final Widget Function(TextStyle)? buildTitle;
final String? message;
final Widget Function(TextStyle)? buildMessage;
final bool outerVerticalPadding;

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);

final baseTitleStyle = TextStyle(
fontSize: 20,
height: 20 / 20,
// More height than in Figma, but it was looking too tight:
// https://github.com/zulip/zulip-flutter/pull/1877#issuecomment-3379664807
// (See use of TextHeightBehavior below.)
height: 24 / 20,
color: designVariables.title,
).merge(weightVariableTextStyle(context, wght: 600));

final effectiveTitle = switch ((buildTitle, title)) {
Widget? effectiveTitle = switch ((buildTitle, title)) {
(final build?, null) => build(baseTitleStyle),
(null, final data?) => Text(style: baseTitleStyle, data),
_ => null,
};

if (effectiveTitle != null) {
effectiveTitle = DefaultTextHeightBehavior(
textHeightBehavior: TextHeightBehavior(
// We want some breathing room between lines,
// without adding margin above or below the title.
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
),
child: effectiveTitle);
}

final baseMessageStyle = TextStyle(
color: designVariables.labelTime,
fontSize: 17,
Expand All @@ -170,12 +210,20 @@ class BottomSheetHeader extends StatelessWidget {
_ => null,
};

return Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 4),
Widget result = Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [?effectiveTitle, ?effectiveMessage]));

if (outerVerticalPadding) {
result = Padding(
padding: EdgeInsets.only(top: 16, bottom: 4),
child: result);
}

return result;
}
}

Expand Down Expand Up @@ -468,7 +516,21 @@ void showChannelActionSheet(BuildContext context, {
[UnsubscribeButton(pageContext: pageContext, channelId: channelId)],
];

_showActionSheet(pageContext, buttonSections: buttonSections);
final header = BottomSheetHeader(
buildTitle: (baseStyle) => Text.rich(
style: baseStyle,
channelTopicLabelSpan(
context: context,
channelId: channelId,
fontSize: baseStyle.fontSize!,
color: baseStyle.color!)),
// TODO(#1896) show channel description
);

_showActionSheet(pageContext,
header: header,
headerScrollable: false,
buttonSections: buttonSections);
}

class SubscribeButton extends ActionSheetMenuItemButton {
Expand Down Expand Up @@ -723,7 +785,20 @@ void showTopicActionSheet(BuildContext context, {
narrow: TopicNarrow(channelId, topic, with_: someMessageIdInTopic),
pageContext: context));

_showActionSheet(pageContext, buttonSections: [optionButtons]);
final header = BottomSheetHeader(
buildTitle: (baseStyle) => Text.rich(
style: baseStyle,
channelTopicLabelSpan(
context: context,
channelId: channelId,
topic: topic,
fontSize: baseStyle.fontSize!,
color: baseStyle.color!)));

_showActionSheet(pageContext,
header: header,
headerScrollable: false,
buttonSections: [optionButtons]);
}

class UserTopicUpdateButton extends ActionSheetMenuItemButton {
Expand Down
7 changes: 3 additions & 4 deletions lib/widgets/emoji_reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -804,10 +804,9 @@ class ViewReactionsHeader extends StatelessWidget {
final reactions = message?.reactions;

if (reactions == null || reactions.aggregated.isEmpty) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: BottomSheetHeader(message: zulipLocalizations.seeWhoReactedSheetNoReactions),
);
return BottomSheetHeader(
outerVerticalPadding: true,
message: zulipLocalizations.seeWhoReactedSheetNoReactions);
}

return Padding(
Expand Down
9 changes: 4 additions & 5 deletions lib/widgets/read_receipts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,10 @@ class _ReadReceiptsHeader extends StatelessWidget {
markup: zulipLocalizations.actionSheetReadReceiptsReadCount(receiptCount));
}

return Padding(
padding: const EdgeInsets.only(top: 8),
child: BottomSheetHeader(
title: zulipLocalizations.actionSheetReadReceipts,
buildMessage: headerMessageBuilder));
return BottomSheetHeader(
outerVerticalPadding: true,
title: zulipLocalizations.actionSheetReadReceipts,
buildMessage: headerMessageBuilder);
}
}

Expand Down
Loading