@@ -18,174 +18,179 @@ import '../model/narrow.dart';
18
18
import 'dialog.dart' ;
19
19
import 'store.dart' ;
20
20
21
- Future <void > markNarrowAsRead (BuildContext context, Narrow narrow) async {
22
- final store = PerAccountStoreWidget .of (context);
23
- final connection = store.connection;
24
- final zulipLocalizations = ZulipLocalizations .of (context);
25
- final useLegacy = connection.zulipFeatureLevel! < 155 ; // TODO(server-6)
26
- if (useLegacy) {
27
- try {
28
- await _legacyMarkNarrowAsRead (context, narrow);
29
- return ;
30
- } catch (e) {
31
- if (! context.mounted) return ;
32
- showErrorDialog (context: context,
33
- title: zulipLocalizations.errorMarkAsReadFailedTitle,
34
- message: e.toString ()); // TODO(#741): extract user-facing message better
35
- return ;
21
+ /// High-level operations that combine API calls with UI feedback.
22
+ ///
23
+ /// Methods in this class provide UI feedback while performing API operations.
24
+ abstract final class ZulipAction {
25
+ static Future <void > markNarrowAsRead (BuildContext context, Narrow narrow) async {
26
+ final store = PerAccountStoreWidget .of (context);
27
+ final connection = store.connection;
28
+ final zulipLocalizations = ZulipLocalizations .of (context);
29
+ final useLegacy = connection.zulipFeatureLevel! < 155 ; // TODO(server-6)
30
+ if (useLegacy) {
31
+ try {
32
+ await _legacyMarkNarrowAsRead (context, narrow);
33
+ return ;
34
+ } catch (e) {
35
+ if (! context.mounted) return ;
36
+ showErrorDialog (context: context,
37
+ title: zulipLocalizations.errorMarkAsReadFailedTitle,
38
+ message: e.toString ()); // TODO(#741): extract user-facing message better
39
+ return ;
40
+ }
36
41
}
37
- }
38
42
39
- final didPass = await updateMessageFlagsStartingFromAnchor (
40
- context: context,
41
- // Include `is:unread` in the narrow. That has a database index, so
42
- // this can be an important optimization in narrows with a lot of history.
43
- // The server applies the same optimization within the (deprecated)
44
- // specialized endpoints for marking messages as read; see
45
- // `do_mark_stream_messages_as_read` in `zulip:zerver/actions/message_flags.py`.
46
- apiNarrow: narrow.apiEncode ()..add (ApiNarrowIs (IsOperand .unread)),
47
- // Use [AnchorCode.oldest], because [AnchorCode.firstUnread]
48
- // will be the oldest non-muted unread message, which would
49
- // result in muted unreads older than the first unread not
50
- // being processed.
51
- anchor: AnchorCode .oldest,
52
- // [AnchorCode.oldest] is an anchor ID lower than any valid
53
- // message ID.
54
- includeAnchor: false ,
55
- op: UpdateMessageFlagsOp .add,
56
- flag: MessageFlag .read,
57
- onCompletedMessage: zulipLocalizations.markAsReadComplete,
58
- progressMessage: zulipLocalizations.markAsReadInProgress,
59
- onFailedTitle: zulipLocalizations.errorMarkAsReadFailedTitle);
43
+ final didPass = await updateMessageFlagsStartingFromAnchor (
44
+ context: context,
45
+ // Include `is:unread` in the narrow. That has a database index, so
46
+ // this can be an important optimization in narrows with a lot of history.
47
+ // The server applies the same optimization within the (deprecated)
48
+ // specialized endpoints for marking messages as read; see
49
+ // `do_mark_stream_messages_as_read` in `zulip:zerver/actions/message_flags.py`.
50
+ apiNarrow: narrow.apiEncode ()..add (ApiNarrowIs (IsOperand .unread)),
51
+ // Use [AnchorCode.oldest], because [AnchorCode.firstUnread]
52
+ // will be the oldest non-muted unread message, which would
53
+ // result in muted unreads older than the first unread not
54
+ // being processed.
55
+ anchor: AnchorCode .oldest,
56
+ // [AnchorCode.oldest] is an anchor ID lower than any valid
57
+ // message ID.
58
+ includeAnchor: false ,
59
+ op: UpdateMessageFlagsOp .add,
60
+ flag: MessageFlag .read,
61
+ onCompletedMessage: zulipLocalizations.markAsReadComplete,
62
+ progressMessage: zulipLocalizations.markAsReadInProgress,
63
+ onFailedTitle: zulipLocalizations.errorMarkAsReadFailedTitle);
60
64
61
- if (! didPass || ! context.mounted) return ;
62
- if (narrow is CombinedFeedNarrow ) {
63
- PerAccountStoreWidget .of (context).unreads.handleAllMessagesReadSuccess ();
65
+ if (! didPass || ! context.mounted) return ;
66
+ if (narrow is CombinedFeedNarrow ) {
67
+ PerAccountStoreWidget .of (context).unreads.handleAllMessagesReadSuccess ();
68
+ }
64
69
}
65
- }
66
70
67
- Future <void > markNarrowAsUnreadFromMessage (
68
- BuildContext context,
69
- Message message,
70
- Narrow narrow,
71
- ) async {
72
- final connection = PerAccountStoreWidget .of (context).connection;
73
- assert (connection.zulipFeatureLevel! >= 155 ); // TODO(server-6)
74
- final zulipLocalizations = ZulipLocalizations .of (context);
75
- await updateMessageFlagsStartingFromAnchor (
76
- context: context,
77
- apiNarrow: narrow.apiEncode (),
78
- anchor: NumericAnchor (message.id),
79
- includeAnchor: true ,
80
- op: UpdateMessageFlagsOp .remove,
81
- flag: MessageFlag .read,
82
- onCompletedMessage: zulipLocalizations.markAsUnreadComplete,
83
- progressMessage: zulipLocalizations.markAsUnreadInProgress,
84
- onFailedTitle: zulipLocalizations.errorMarkAsUnreadFailedTitle);
85
- }
71
+ /// Add or remove the given flag from the anchor to the end of the narrow,
72
+ /// showing feedback to the user on progress or failure.
73
+ ///
74
+ /// This has the semantics of [updateMessageFlagsForNarrow]
75
+ /// (see https://zulip.com/api/update-message-flags-for-narrow)
76
+ /// with `numBefore: 0` and infinite `numAfter` . It operates by calling that
77
+ /// endpoint with a finite `numAfter` as a batch size, in a loop.
78
+ ///
79
+ /// If the operation requires more than one batch, the user is shown progress
80
+ /// feedback through [SnackBar] , using [progressMessage] and [onCompletedMessage] .
81
+ /// If the operation fails, the user is shown an error dialog box with title
82
+ /// [onFailedTitle] .
83
+ ///
84
+ /// Returns true just if the operation finished successfully.
85
+ static Future <bool > updateMessageFlagsStartingFromAnchor ({
86
+ required BuildContext context,
87
+ required List <ApiNarrowElement > apiNarrow,
88
+ required Anchor anchor,
89
+ required bool includeAnchor,
90
+ required UpdateMessageFlagsOp op,
91
+ required MessageFlag flag,
92
+ required String Function (int ) onCompletedMessage,
93
+ required String progressMessage,
94
+ required String onFailedTitle,
95
+ }) async {
96
+ try {
97
+ final store = PerAccountStoreWidget .of (context);
98
+ final connection = store.connection;
99
+ final scaffoldMessenger = ScaffoldMessenger .of (context);
86
100
87
- /// Add or remove the given flag from the anchor to the end of the narrow,
88
- /// showing feedback to the user on progress or failure.
89
- ///
90
- /// This has the semantics of [updateMessageFlagsForNarrow]
91
- /// (see https://zulip.com/api/update-message-flags-for-narrow)
92
- /// with `numBefore: 0` and infinite `numAfter` . It operates by calling that
93
- /// endpoint with a finite `numAfter` as a batch size, in a loop.
94
- ///
95
- /// If the operation requires more than one batch, the user is shown progress
96
- /// feedback through [SnackBar] , using [progressMessage] and [onCompletedMessage] .
97
- /// If the operation fails, the user is shown an error dialog box with title
98
- /// [onFailedTitle] .
99
- ///
100
- /// Returns true just if the operation finished successfully.
101
- Future <bool > updateMessageFlagsStartingFromAnchor ({
102
- required BuildContext context,
103
- required List <ApiNarrowElement > apiNarrow,
104
- required Anchor anchor,
105
- required bool includeAnchor,
106
- required UpdateMessageFlagsOp op,
107
- required MessageFlag flag,
108
- required String Function (int ) onCompletedMessage,
109
- required String progressMessage,
110
- required String onFailedTitle,
111
- }) async {
112
- try {
113
- final store = PerAccountStoreWidget .of (context);
114
- final connection = store.connection;
115
- final scaffoldMessenger = ScaffoldMessenger .of (context);
101
+ // Compare web's `mark_all_as_read` in web/src/unread_ops.js
102
+ // and zulip-mobile's `markAsUnreadFromMessage` in src/action-sheets/index.js .
103
+ int responseCount = 0 ;
104
+ int updatedCount = 0 ;
105
+ while (true ) {
106
+ final result = await updateMessageFlagsForNarrow (connection,
107
+ anchor: anchor,
108
+ includeAnchor: includeAnchor,
109
+ // There is an upper limit of 5000 messages per batch
110
+ // (numBefore + numAfter <= 5000) enforced on the server.
111
+ // See `update_message_flags_in_narrow` in zerver/views/message_flags.py .
112
+ // zulip-mobile uses `numAfter` of 5000, but web uses 1000
113
+ // for more responsive feedback. See zulip@f0d87fcf6.
114
+ numBefore: 0 ,
115
+ numAfter: 1000 ,
116
+ narrow: apiNarrow,
117
+ op: op,
118
+ flag: flag);
119
+ if (! context.mounted) {
120
+ scaffoldMessenger.clearSnackBars ();
121
+ return false ;
122
+ }
123
+ responseCount++ ;
124
+ updatedCount += result.updatedCount;
116
125
117
- // Compare web's `mark_all_as_read` in web/src/unread_ops.js
118
- // and zulip-mobile's `markAsUnreadFromMessage` in src/action-sheets/index.js .
119
- int responseCount = 0 ;
120
- int updatedCount = 0 ;
121
- while (true ) {
122
- final result = await updateMessageFlagsForNarrow (connection,
123
- anchor: anchor,
124
- includeAnchor: includeAnchor,
125
- // There is an upper limit of 5000 messages per batch
126
- // (numBefore + numAfter <= 5000) enforced on the server.
127
- // See `update_message_flags_in_narrow` in zerver/views/message_flags.py .
128
- // zulip-mobile uses `numAfter` of 5000, but web uses 1000
129
- // for more responsive feedback. See zulip@f0d87fcf6.
130
- numBefore: 0 ,
131
- numAfter: 1000 ,
132
- narrow: apiNarrow,
133
- op: op,
134
- flag: flag);
135
- if (! context.mounted) {
136
- scaffoldMessenger.clearSnackBars ();
137
- return false ;
138
- }
139
- responseCount++ ;
140
- updatedCount += result.updatedCount;
126
+ if (result.foundNewest) {
127
+ if (responseCount > 1 ) {
128
+ // We previously showed an in-progress [SnackBar], so say we're done.
129
+ // There may be a backlog of [SnackBar]s accumulated in the queue
130
+ // so be sure to clear them out here.
131
+ scaffoldMessenger
132
+ ..clearSnackBars ()
133
+ ..showSnackBar (SnackBar (behavior: SnackBarBehavior .floating,
134
+ content: Text (onCompletedMessage (updatedCount))));
135
+ }
136
+ return true ;
137
+ }
141
138
142
- if (result.foundNewest ) {
143
- if (responseCount > 1 ) {
144
- // We previously showed an in-progress [SnackBar], so say we're done .
145
- // There may be a backlog of [SnackBar]s accumulated in the queue
146
- // so be sure to clear them out here.
147
- scaffoldMessenger
148
- .. clearSnackBars ()
149
- .. showSnackBar ( SnackBar (behavior : SnackBarBehavior .floating,
150
- content : Text ( onCompletedMessage (updatedCount)))) ;
139
+ if (result.lastProcessedId == null ) {
140
+ final zulipLocalizations = ZulipLocalizations . of (context);
141
+ // No messages were in the range of the request .
142
+ // This should be impossible given that `foundNewest` was false
143
+ // (and that our `numAfter` was positive.)
144
+ showErrorDialog (context : context,
145
+ title : onFailedTitle,
146
+ message : zulipLocalizations.errorInvalidResponse);
147
+ return false ;
151
148
}
152
- return true ;
153
- }
149
+ anchor = NumericAnchor (result.lastProcessedId ! ) ;
150
+ includeAnchor = false ;
154
151
155
- if (result.lastProcessedId == null ) {
156
- final zulipLocalizations = ZulipLocalizations .of (context);
157
- // No messages were in the range of the request.
158
- // This should be impossible given that `foundNewest` was false
159
- // (and that our `numAfter` was positive.)
160
- showErrorDialog (context: context,
161
- title: onFailedTitle,
162
- message: zulipLocalizations.errorInvalidResponse);
163
- return false ;
152
+ // The task is taking a while, so tell the user we're working on it.
153
+ // TODO: Ideally we'd have a progress widget here that showed up based
154
+ // on actual time elapsed -- so it could appear before the first
155
+ // batch returns, if that takes a while -- and that then stuck
156
+ // around continuously until the task ends. For now we use a
157
+ // series of [SnackBar]s, which may feel a bit janky.
158
+ // There is complexity in tracking the status of each [SnackBar],
159
+ // due to having no way to determine which is currently active,
160
+ // or if there is an active one at all. Resetting the [SnackBar] here
161
+ // results in the same message popping in and out and the user experience
162
+ // is better for now if we allow them to run their timer through
163
+ // and clear the backlog later.
164
+ scaffoldMessenger.showSnackBar (SnackBar (behavior: SnackBarBehavior .floating,
165
+ content: Text (progressMessage)));
164
166
}
165
- anchor = NumericAnchor (result.lastProcessedId! );
166
- includeAnchor = false ;
167
-
168
- // The task is taking a while, so tell the user we're working on it.
169
- // TODO: Ideally we'd have a progress widget here that showed up based
170
- // on actual time elapsed -- so it could appear before the first
171
- // batch returns, if that takes a while -- and that then stuck
172
- // around continuously until the task ends. For now we use a
173
- // series of [SnackBar]s, which may feel a bit janky.
174
- // There is complexity in tracking the status of each [SnackBar],
175
- // due to having no way to determine which is currently active,
176
- // or if there is an active one at all. Resetting the [SnackBar] here
177
- // results in the same message popping in and out and the user experience
178
- // is better for now if we allow them to run their timer through
179
- // and clear the backlog later.
180
- scaffoldMessenger.showSnackBar (SnackBar (behavior: SnackBarBehavior .floating,
181
- content: Text (progressMessage)));
167
+ } catch (e) {
168
+ if (! context.mounted) return false ;
169
+ showErrorDialog (context: context,
170
+ title: onFailedTitle,
171
+ message: e.toString ()); // TODO(#741): extract user-facing message better
172
+ return false ;
182
173
}
183
- } catch (e) {
184
- if (! context.mounted) return false ;
185
- showErrorDialog (context: context,
186
- title: onFailedTitle,
187
- message: e.toString ()); // TODO(#741): extract user-facing message better
188
- return false ;
174
+ }
175
+
176
+ static Future <void > markNarrowAsUnreadFromMessage (
177
+ BuildContext context,
178
+ Message message,
179
+ Narrow narrow,
180
+ ) async {
181
+ final connection = PerAccountStoreWidget .of (context).connection;
182
+ assert (connection.zulipFeatureLevel! >= 155 ); // TODO(server-6)
183
+ final zulipLocalizations = ZulipLocalizations .of (context);
184
+ await updateMessageFlagsStartingFromAnchor (
185
+ context: context,
186
+ apiNarrow: narrow.apiEncode (),
187
+ anchor: NumericAnchor (message.id),
188
+ includeAnchor: true ,
189
+ op: UpdateMessageFlagsOp .remove,
190
+ flag: MessageFlag .read,
191
+ onCompletedMessage: zulipLocalizations.markAsUnreadComplete,
192
+ progressMessage: zulipLocalizations.markAsUnreadInProgress,
193
+ onFailedTitle: zulipLocalizations.errorMarkAsUnreadFailedTitle);
189
194
}
190
195
}
191
196
0 commit comments