Skip to content

Commit 632c720

Browse files
committed
action_sheet: Add "Quote and reply" button
Fixes: zulip#116
1 parent c9ac25c commit 632c720

File tree

1 file changed

+105
-2
lines changed

1 file changed

+105
-2
lines changed

lib/widgets/action_sheet.dart

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,31 @@ import 'package:flutter/material.dart';
22
import 'package:share_plus/share_plus.dart';
33

44
import '../api/model/model.dart';
5+
import '../api/route/messages.dart';
6+
import 'dialog.dart';
57
import 'draggable_scrollable_modal_bottom_sheet.dart';
8+
import 'message_list.dart';
9+
import 'store.dart';
610

11+
/// Show a sheet of actions you can take on a message in the message list.
12+
///
13+
/// Must have a [MessageListPage] ancestor.
714
void showMessageActionSheet({required BuildContext context, required Message message}) {
15+
// The UI that's conditioned on this won't live-update during this appearance
16+
// of the action sheet (we avoid calling composeBoxControllerOf in a build
17+
// method; see its doc). But currently it will be constant through the life of
18+
// any message list, so that's fine.
19+
final isComposeBoxOffered = MessageListPageState.composeBoxControllerOf(context) != null;
820
showDraggableScrollableModalBottomSheet(
921
context: context,
1022
builder: (BuildContext innerContext) {
1123
return Column(children: [
12-
ShareButton(message: message, bottomSheetContext: innerContext),
24+
ShareButton(message: message, bottomSheetContext: innerContext, messageListContext: context),
25+
if (isComposeBoxOffered) QuoteAndReplyButton(
26+
message: message,
27+
bottomSheetContext: innerContext,
28+
messageListContext: context,
29+
),
1330
]);
1431
});
1532
}
@@ -19,14 +36,17 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
1936
super.key,
2037
required this.message,
2138
required this.bottomSheetContext,
22-
}) : assert(bottomSheetContext.findAncestorWidgetOfExactType<BottomSheet>() != null);
39+
required this.messageListContext,
40+
}) : assert(bottomSheetContext.findAncestorWidgetOfExactType<BottomSheet>() != null),
41+
assert(messageListContext.findAncestorWidgetOfExactType<MessageListPage>() != null);
2342

2443
IconData get icon;
2544
String get label;
2645
VoidCallback get onPressed;
2746

2847
final Message message;
2948
final BuildContext bottomSheetContext;
49+
final BuildContext messageListContext;
3050

3151
@override
3252
Widget build(BuildContext context) {
@@ -42,6 +62,7 @@ class ShareButton extends MessageActionSheetMenuItemButton {
4262
super.key,
4363
required super.message,
4464
required super.bottomSheetContext,
65+
required super.messageListContext,
4566
});
4667

4768
@override IconData get icon => Icons.adaptive.share;
@@ -68,3 +89,85 @@ class ShareButton extends MessageActionSheetMenuItemButton {
6889
await Share.shareWithResult(message.content);
6990
};
7091
}
92+
93+
class MessageNotFoundException implements Exception {}
94+
95+
class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
96+
QuoteAndReplyButton({
97+
super.key,
98+
required super.message,
99+
required super.bottomSheetContext,
100+
required super.messageListContext,
101+
});
102+
103+
@override IconData get icon => Icons.format_quote;
104+
105+
@override String get label => 'Quote and reply';
106+
107+
@override VoidCallback get onPressed => () async {
108+
// Close the message action sheet. We'll show the request progress
109+
// in the compose-box content input with a "[Quoting…]" placeholder.
110+
Navigator.of(bottomSheetContext).pop();
111+
112+
// This will be null only if the compose box disappeared after the
113+
// message action sheet opened, and before "Quote and reply" was pressed.
114+
// Currently a compose box can't ever disappear, so this is impossible.
115+
final composeBoxController = MessageListPageState.composeBoxControllerOf(messageListContext);
116+
final topicController = composeBoxController!.topicController;
117+
if (
118+
topicController != null
119+
&& topicController.textNormalized == kNoTopicTopic
120+
&& message is StreamMessage
121+
) {
122+
topicController.value = TextEditingValue(text: message.subject);
123+
}
124+
final tag = composeBoxController.contentController
125+
.registerQuoteAndReplyStart(PerAccountStoreWidget.of(messageListContext),
126+
message: message,
127+
);
128+
129+
Message? fetchedMessage;
130+
// TODO, supported by reusable code:
131+
// - (?) Retry with backoff on plausibly transient errors.
132+
// - If request(s) take(s) a long time, show snackbar with cancel
133+
// button, like "Still working on quote-and-reply…".
134+
// On final failure or success, auto-dismiss the snackbar.
135+
try {
136+
fetchedMessage = await getMessageCompat(PerAccountStoreWidget.of(messageListContext).connection,
137+
messageId: message.id,
138+
applyMarkdown: false,
139+
);
140+
if (fetchedMessage == null) {
141+
throw MessageNotFoundException();
142+
}
143+
} catch (e) {
144+
if (!bottomSheetContext.mounted) return;
145+
String? errorMessage;
146+
if (e is MessageNotFoundException) {
147+
errorMessage = 'Message not found.';
148+
}
149+
// TODO specific messages for common errors, like network errors
150+
// (support with reusable code)
151+
// TODO(?) give no feedback on error conditions we expect to
152+
// flag centrally in event polling, like invalid auth,
153+
// user/realm deactivated. (Support with reusable code.)
154+
showErrorDialog(context: bottomSheetContext,
155+
title: 'Quotation failed', message: errorMessage);
156+
} finally {
157+
if (messageListContext.mounted) {
158+
// This will be null only if the compose box disappeared during the
159+
// quotation request. Currently a compose box can't ever disappear,
160+
// so this is impossible.
161+
final composeBoxController = MessageListPageState.composeBoxControllerOf(messageListContext);
162+
composeBoxController!.contentController
163+
.registerQuoteAndReplyEnd(PerAccountStoreWidget.of(messageListContext), tag,
164+
message: message,
165+
rawContent: fetchedMessage?.content,
166+
);
167+
if (!composeBoxController.contentFocusNode.hasFocus) {
168+
composeBoxController.contentFocusNode.requestFocus();
169+
}
170+
}
171+
}
172+
};
173+
}

0 commit comments

Comments
 (0)