Skip to content

Commit 6a2649a

Browse files
committed
action_sheet: Add header param; use it to show message content
See Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3483-29966&m=dev See also a different header variant in Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev At first I'd thought this wouldn't be possible without more work toward issue #488: - #488 "content: Support Zulip content outside messages (even outside per-account contexts)". But it turns out that our content widgets don't assume a MessageList ancestor; it's sufficient that we provide a per-account context, via PerAccountStoreWidget, and a per-message context, via InheritedMessage (created by MessageContent). Fixes: #217
1 parent 27d124d commit 6a2649a

File tree

2 files changed

+99
-3
lines changed

2 files changed

+99
-3
lines changed

lib/widgets/action_sheet.dart

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import '../api/route/channels.dart';
1313
import '../api/route/messages.dart';
1414
import '../generated/l10n/zulip_localizations.dart';
1515
import '../model/binding.dart';
16+
import '../model/content.dart';
1617
import '../model/emoji.dart';
1718
import '../model/internal_link.dart';
1819
import '../model/narrow.dart';
1920
import 'actions.dart';
2021
import 'color.dart';
2122
import 'compose_box.dart';
23+
import 'content.dart';
2224
import 'dialog.dart';
2325
import 'emoji.dart';
2426
import 'emoji_reaction.dart';
@@ -34,6 +36,8 @@ import 'topic_list.dart';
3436
void _showActionSheet(
3537
BuildContext pageContext, {
3638
required List<Widget> optionButtons,
39+
Widget? header,
40+
Color? headerBackgroundColor,
3741
}) {
3842
// Could omit this if we need _showActionSheet outside a per-account context.
3943
final store = PerAccountStoreWidget.of(pageContext);
@@ -57,15 +61,33 @@ void _showActionSheet(
5761
child: Column(
5862
mainAxisSize: MainAxisSize.min,
5963
children: [
60-
SizedBox(height: 8),
64+
if (header != null)
65+
Flexible(
66+
// TODO(upstream) Enforce a flex ratio (e.g. 1:3)
67+
// only when the header height plus the buttons' height
68+
// exceeds available space. Otherwise let one or the other
69+
// grow to fill available space even if it breaks the ratio.
70+
// Needs support for separate properties like `flex-grow`
71+
// and `flex-shrink`.
72+
flex: 1,
73+
child: InsetShadowBox(
74+
top: 8, bottom: 8,
75+
color: designVariables.bgContextMenu,
76+
child: SingleChildScrollView(
77+
padding: EdgeInsets.symmetric(vertical: 8),
78+
child: ColoredBox(
79+
color: headerBackgroundColor ?? Colors.transparent,
80+
child: header))))
81+
else
82+
SizedBox(height: 8),
6183
Flexible(
84+
flex: 3,
6285
child: Padding(
6386
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
6487
child: Column(
6588
crossAxisAlignment: CrossAxisAlignment.stretch,
6689
mainAxisSize: MainAxisSize.min,
6790
children: [
68-
// TODO(#217): show message text
6991
Flexible(child: InsetShadowBox(
7092
top: 8, bottom: 8,
7193
color: designVariables.bgContextMenu,
@@ -616,7 +638,42 @@ void showMessageActionSheet({required BuildContext context, required Message mes
616638
EditButton(message: message, pageContext: pageContext),
617639
];
618640

619-
_showActionSheet(pageContext, optionButtons: optionButtons);
641+
_showActionSheet(pageContext,
642+
optionButtons: optionButtons,
643+
header: _MessageActionSheetHeader(message: message));
644+
}
645+
646+
class _MessageActionSheetHeader extends StatelessWidget {
647+
const _MessageActionSheetHeader({required this.message});
648+
649+
final Message message;
650+
651+
@override
652+
Widget build(BuildContext context) {
653+
final designVariables = DesignVariables.of(context);
654+
655+
// TODO this seems to lose the hero animation when opening an image;
656+
// investigate.
657+
// TODO should we close the sheet before opening a narrow link?
658+
// On popping the pushed narrow route, the sheet is still open.
659+
660+
return Container(
661+
// TODO(#647) use different color for highlighted messages
662+
// TODO(#681) use different color for DM messages
663+
color: designVariables.bgMessageRegular,
664+
padding: EdgeInsets.symmetric(vertical: 4),
665+
child: Column(
666+
mainAxisSize: MainAxisSize.max,
667+
spacing: 4,
668+
children: [
669+
SenderRow(message: message, showTimestamp: true),
670+
Padding(
671+
padding: const EdgeInsets.symmetric(horizontal: 16),
672+
// TODO(#10) offer text selection; the Figma asks for it here:
673+
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3483-30210&m=dev
674+
child: MessageContent(message: message, content: parseMessageContent(message))),
675+
]));
676+
}
620677
}
621678

622679
abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButton {

test/widgets/action_sheet_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import '../api/fake_api.dart';
3838
import '../example_data.dart' as eg;
3939
import '../flutter_checks.dart';
4040
import '../model/binding.dart';
41+
import '../model/content_test.dart';
4142
import '../model/test_store.dart';
4243
import '../stdlib_checks.dart';
4344
import '../test_clipboard.dart';
@@ -854,6 +855,44 @@ void main() {
854855
});
855856

856857
group('message action sheet', () {
858+
group('header', () {
859+
testWidgets('message sender and content shown', (tester) async {
860+
final message = eg.streamMessage(content: ContentExample.userMentionPlain.html);
861+
await setupToMessageActionSheet(tester,
862+
message: message,
863+
narrow: TopicNarrow.ofMessage(message));
864+
check(find.descendant(
865+
of: find.byType(BottomSheet),
866+
matching: find.byWidgetPredicate(
867+
(widget) => widget is Avatar && widget.userId == message.senderId))
868+
).findsOne();
869+
check(find.descendant(
870+
of: find.byType(BottomSheet),
871+
matching: find.byType(UserMention))
872+
).findsOne();
873+
});
874+
875+
testWidgets('poll is rendered', (tester) async {
876+
final submessageContent = eg.pollWidgetData(
877+
question: 'poll', options: ['First option', 'Second option']);
878+
final message = eg.streamMessage(
879+
sender: eg.selfUser,
880+
submessages: [eg.submessage(content: submessageContent)]);
881+
await setupToMessageActionSheet(tester,
882+
message: message,
883+
narrow: TopicNarrow.ofMessage(message));
884+
check(find.descendant(
885+
of: find.byType(BottomSheet),
886+
matching: find.byWidgetPredicate(
887+
(widget) => widget is Avatar && widget.userId == message.senderId))
888+
).findsOne();
889+
check(find.descendant(
890+
of: find.byType(BottomSheet),
891+
matching: find.text('First option'))
892+
).findsOne();
893+
});
894+
});
895+
857896
group('ReactionButtons', () {
858897
testWidgets('absent if ServerEmojiData not loaded', (tester) async {
859898
final message = eg.streamMessage();

0 commit comments

Comments
 (0)