Skip to content

Commit fa7b738

Browse files
author
chimnayajith
committed
actions [nfc]: Move markNarrowAsRead and updateMessageFlagsStartingFromAnchor functios into new ZulipAction abstract class
Move the high level operation markNarrowAsRead and updateMessageFlagsStartingFromAnchor into the new ZulipAction abstract class, per discussion in: https://chat.zulip.org/#narrow/channel/516-mobile-dev-help/topic/.23F1317.20showErrorDialog/near/2080576 This makes it clearer at call sites that the methods combine API operations with UI feedback.
1 parent 0417c87 commit fa7b738

File tree

3 files changed

+167
-162
lines changed

3 files changed

+167
-162
lines changed

lib/widgets/actions.dart

Lines changed: 157 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,162 @@ import '../notifications/receive.dart';
2020
import 'dialog.dart';
2121
import '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+
23179
Future<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-
102212
Future<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-
227232
Future<void> _legacyMarkNarrowAsRead(BuildContext context, Narrow narrow) async {
228233
final store = PerAccountStoreWidget.of(context);
229234
final connection = store.connection;

lib/widgets/message_list.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ class _MarkAsReadWidgetState extends State<MarkAsReadWidget> {
802802
void _handlePress(BuildContext context) async {
803803
if (!context.mounted) return;
804804
setState(() => _loading = true);
805-
await markNarrowAsRead(context, widget.narrow);
805+
await ZulipAction.markNarrowAsRead(context, widget.narrow);
806806
setState(() => _loading = false);
807807
}
808808

0 commit comments

Comments
 (0)