@@ -18,174 +18,179 @@ import '../model/narrow.dart';
1818import 'dialog.dart' ;
1919import 'store.dart' ;
2020
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+ }
3641 }
37- }
3842
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);
6064
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+ }
6469 }
65- }
6670
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);
86100
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;
116125
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+ }
141138
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 ;
151148 }
152- return true ;
153- }
149+ anchor = NumericAnchor (result.lastProcessedId ! ) ;
150+ includeAnchor = false ;
154151
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)));
164166 }
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 ;
182173 }
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);
189194 }
190195}
191196
0 commit comments