1
1
import 'package:flutter/material.dart' ;
2
2
import 'package:share_plus/share_plus.dart' ;
3
3
4
+ import '../api/exception.dart' ;
4
5
import '../api/model/model.dart' ;
6
+ import '../api/route/messages.dart' ;
7
+ import 'compose_box.dart' ;
8
+ import 'dialog.dart' ;
5
9
import 'draggable_scrollable_modal_bottom_sheet.dart' ;
10
+ import 'message_list.dart' ;
11
+ import 'store.dart' ;
6
12
13
+ /// Show a sheet of actions you can take on a message in the message list.
14
+ ///
15
+ /// Must have a [MessageListPage] ancestor.
7
16
void showMessageActionSheet ({required BuildContext context, required Message message}) {
17
+ // The UI that's conditioned on this won't live-update during this appearance
18
+ // of the action sheet (we avoid calling composeBoxControllerOf in a build
19
+ // method; see its doc). But currently it will be constant through the life of
20
+ // any message list, so that's fine.
21
+ final isComposeBoxOffered = MessageListPageState .composeBoxControllerOf (context) != null ;
8
22
showDraggableScrollableModalBottomSheet (
9
23
context: context,
10
24
builder: (BuildContext innerContext) {
11
25
return Column (children: [
12
- ShareButton (message: message, bottomSheetContext: innerContext),
26
+ ShareButton (message: message, bottomSheetContext: innerContext, messageListContext: context),
27
+ if (isComposeBoxOffered) QuoteAndReplyButton (
28
+ message: message,
29
+ bottomSheetContext: innerContext,
30
+ messageListContext: context,
31
+ ),
13
32
]);
14
33
});
15
34
}
@@ -19,14 +38,17 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
19
38
super .key,
20
39
required this .message,
21
40
required this .bottomSheetContext,
22
- }) : assert (bottomSheetContext.findAncestorWidgetOfExactType <BottomSheet >() != null );
41
+ required this .messageListContext,
42
+ }) : assert (bottomSheetContext.findAncestorWidgetOfExactType <BottomSheet >() != null ),
43
+ assert (messageListContext.findAncestorWidgetOfExactType <MessageListPage >() != null );
23
44
24
45
IconData get icon;
25
46
String get label;
26
47
VoidCallback get onPressed;
27
48
28
49
final Message message;
29
50
final BuildContext bottomSheetContext;
51
+ final BuildContext messageListContext;
30
52
31
53
@override
32
54
Widget build (BuildContext context) {
@@ -42,6 +64,7 @@ class ShareButton extends MessageActionSheetMenuItemButton {
42
64
super .key,
43
65
required super .message,
44
66
required super .bottomSheetContext,
67
+ required super .messageListContext,
45
68
});
46
69
47
70
@override IconData get icon => Icons .adaptive.share;
@@ -68,3 +91,92 @@ class ShareButton extends MessageActionSheetMenuItemButton {
68
91
await Share .shareWithResult (message.content);
69
92
};
70
93
}
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_outlined;
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
+ ComposeBoxController ? 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
+ String ? errorMessage;
131
+ // TODO, supported by reusable code:
132
+ // - (?) Retry with backoff on plausibly transient errors.
133
+ // - If request(s) take(s) a long time, show snackbar with cancel
134
+ // button, like "Still working on quote-and-reply…".
135
+ // On final failure or success, auto-dismiss the snackbar.
136
+ try {
137
+ fetchedMessage = await getMessageCompat (PerAccountStoreWidget .of (messageListContext).connection,
138
+ messageId: message.id,
139
+ applyMarkdown: false ,
140
+ );
141
+ if (fetchedMessage == null ) {
142
+ errorMessage = 'That message does not seem to exist.' ;
143
+ }
144
+ } catch (e) {
145
+ switch (e) {
146
+ case ZulipApiException ():
147
+ errorMessage = e.message;
148
+ }
149
+ }
150
+
151
+ if (fetchedMessage == null && errorMessage == null ) {
152
+ errorMessage = 'Could not fetch message source.' ;
153
+ }
154
+
155
+ if (! messageListContext.mounted) return ;
156
+
157
+ if (fetchedMessage == null ) {
158
+ // TODO specific messages for common errors, like network errors
159
+ // (support with reusable code)
160
+ // TODO(?) give no feedback on error conditions we expect to
161
+ // flag centrally in event polling, like invalid auth,
162
+ // user/realm deactivated. (Support with reusable code.)
163
+ await showErrorDialog (context: messageListContext,
164
+ title: 'Quotation failed' , message: errorMessage);
165
+ }
166
+
167
+ if (! messageListContext.mounted) return ;
168
+
169
+ // This will be null only if the compose box disappeared during the
170
+ // quotation request. Currently a compose box can't ever disappear,
171
+ // so this is impossible.
172
+ composeBoxController = MessageListPageState .composeBoxControllerOf (messageListContext);
173
+ composeBoxController! .contentController
174
+ .registerQuoteAndReplyEnd (PerAccountStoreWidget .of (messageListContext), tag,
175
+ message: message,
176
+ rawContent: fetchedMessage? .content,
177
+ );
178
+ if (! composeBoxController.contentFocusNode.hasFocus) {
179
+ composeBoxController.contentFocusNode.requestFocus ();
180
+ }
181
+ };
182
+ }
0 commit comments