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