Skip to content

Commit 6a27b90

Browse files
chrisbobbegithub-actions[bot]
authored andcommitted
action_sheet: Implement resolve/unresolve in topic action sheet
Fixes: #744
1 parent 567d968 commit 6a27b90

14 files changed

+359
-10
lines changed

assets/l10n/app_en.arb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@
9292
"@actionSheetOptionUnfollowTopic": {
9393
"description": "Label for unfollowing a topic on action sheet."
9494
},
95+
"actionSheetOptionResolveTopic": "Mark as resolved",
96+
"@actionSheetOptionResolveTopic": {
97+
"description": "Label for the 'Mark as resolved' button on the topic action sheet."
98+
},
99+
"actionSheetOptionUnresolveTopic": "Mark as unresolved",
100+
"@actionSheetOptionUnresolveTopic": {
101+
"description": "Label for the 'Mark as unresolved' button on the topic action sheet."
102+
},
103+
"errorResolveTopicFailedTitle": "Failed to mark topic as resolved",
104+
"@errorResolveTopicFailedTitle": {
105+
"description": "Error title when marking a topic as resolved failed."
106+
},
107+
"errorUnresolveTopicFailedTitle": "Failed to mark topic as unresolved",
108+
"@errorUnresolveTopicFailedTitle": {
109+
"description": "Error title when marking a topic as unresolved failed."
110+
},
95111
"actionSheetOptionCopyMessageText": "Copy message text",
96112
"@actionSheetOptionCopyMessageText": {
97113
"description": "Label for copy message text button on action sheet."

lib/api/model/model.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,12 @@ extension type const TopicName(String _value) {
695695
/// The key to use for "same topic as" comparisons.
696696
String canonicalize() => apiName.toLowerCase();
697697

698+
/// Whether the topic starts with [resolvedTopicPrefix].
699+
bool get isResolved => _value.startsWith(resolvedTopicPrefix);
700+
701+
/// This [TopicName] plus the [resolvedTopicPrefix] prefix.
702+
TopicName resolve() => TopicName(resolvedTopicPrefix + _value);
703+
698704
/// A [TopicName] with [resolvedTopicPrefixRegexp] stripped if present.
699705
TopicName unresolve() =>
700706
TopicName(_value.replaceFirst(resolvedTopicPrefixRegexp, ''));

lib/generated/l10n/zulip_localizations.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,30 @@ abstract class ZulipLocalizations {
243243
/// **'Unfollow topic'**
244244
String get actionSheetOptionUnfollowTopic;
245245

246+
/// Label for the 'Mark as resolved' button on the topic action sheet.
247+
///
248+
/// In en, this message translates to:
249+
/// **'Mark as resolved'**
250+
String get actionSheetOptionResolveTopic;
251+
252+
/// Label for the 'Mark as unresolved' button on the topic action sheet.
253+
///
254+
/// In en, this message translates to:
255+
/// **'Mark as unresolved'**
256+
String get actionSheetOptionUnresolveTopic;
257+
258+
/// Error title when marking a topic as resolved failed.
259+
///
260+
/// In en, this message translates to:
261+
/// **'Failed to mark topic as resolved'**
262+
String get errorResolveTopicFailedTitle;
263+
264+
/// Error title when marking a topic as unresolved failed.
265+
///
266+
/// In en, this message translates to:
267+
/// **'Failed to mark topic as unresolved'**
268+
String get errorUnresolveTopicFailedTitle;
269+
246270
/// Label for copy message text button on action sheet.
247271
///
248272
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Unfollow topic';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Copy message text';
8496

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Unfollow topic';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Copy message text';
8496

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Unfollow topic';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Copy message text';
8496

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Unfollow topic';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Copy message text';
8496

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Nie śledź wątku';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Skopiuj tekst wiadomości';
8496

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Не отслеживать тему';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Скопировать текст сообщения';
8496

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
7979
@override
8080
String get actionSheetOptionUnfollowTopic => 'Prestať sledovať tému';
8181

82+
@override
83+
String get actionSheetOptionResolveTopic => 'Mark as resolved';
84+
85+
@override
86+
String get actionSheetOptionUnresolveTopic => 'Mark as unresolved';
87+
88+
@override
89+
String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved';
90+
91+
@override
92+
String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved';
93+
8294
@override
8395
String get actionSheetOptionCopyMessageText => 'Skopírovať text správy';
8496

lib/widgets/action_sheet.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,13 @@ class ActionSheetCancelButton extends StatelessWidget {
166166
/// Show a sheet of actions you can take on a topic.
167167
///
168168
/// Needs a [PageRoot] ancestor.
169+
///
170+
/// The API request for resolving/unresolving a topic needs a message ID.
171+
/// If [someMessageIdInTopic] is null, the button for that will be absent.
169172
void showTopicActionSheet(BuildContext context, {
170173
required int channelId,
171174
required TopicName topic,
175+
required int? someMessageIdInTopic,
172176
}) {
173177
final pageContext = PageRoot.contextOf(context);
174178

@@ -245,6 +249,12 @@ void showTopicActionSheet(BuildContext context, {
245249
pageContext: pageContext);
246250
}));
247251

252+
if (someMessageIdInTopic != null) {
253+
optionButtons.add(ResolveUnresolveButton(pageContext: pageContext,
254+
topic: topic,
255+
someMessageIdInTopic: someMessageIdInTopic));
256+
}
257+
248258
if (optionButtons.isEmpty) {
249259
// TODO(a11y): This case makes a no-op gesture handler; as a consequence,
250260
// we're presenting some UI (to people who use screen-reader software) as
@@ -377,6 +387,80 @@ class UserTopicUpdateButton extends ActionSheetMenuItemButton {
377387
}
378388
}
379389

390+
class ResolveUnresolveButton extends ActionSheetMenuItemButton {
391+
ResolveUnresolveButton({
392+
super.key,
393+
required this.topic,
394+
required this.someMessageIdInTopic,
395+
required super.pageContext,
396+
}) : _actionIsResolve = !topic.isResolved;
397+
398+
/// The topic that the action sheet was opened for.
399+
///
400+
/// There might not currently be any messages with this topic;
401+
/// see dartdoc of [ActionSheetMenuItemButton].
402+
final TopicName topic;
403+
404+
/// The message ID that was passed when opening the action sheet.
405+
///
406+
/// The message with this ID might currently not exist,
407+
/// or might exist with a different topic;
408+
/// see dartdoc of [ActionSheetMenuItemButton].
409+
final int someMessageIdInTopic;
410+
411+
final bool _actionIsResolve;
412+
413+
@override
414+
IconData get icon => _actionIsResolve ? ZulipIcons.check : ZulipIcons.check_remove;
415+
416+
@override
417+
String label(ZulipLocalizations zulipLocalizations) {
418+
return _actionIsResolve
419+
? zulipLocalizations.actionSheetOptionResolveTopic
420+
: zulipLocalizations.actionSheetOptionUnresolveTopic;
421+
}
422+
423+
@override void onPressed() async {
424+
final zulipLocalizations = ZulipLocalizations.of(pageContext);
425+
final store = PerAccountStoreWidget.of(pageContext);
426+
427+
// We *could* check here if the topic has changed since the action sheet was
428+
// opened (see dartdoc of [ActionSheetMenuItemButton]) and abort if so.
429+
// We simplify by not doing so.
430+
// There's already an inherent race that that check wouldn't help with:
431+
// when you tap the button, an intervening topic change may already have
432+
// happened, just not reached us in an event yet.
433+
// Discussion, including about what web does:
434+
// https://github.com/zulip/zulip-flutter/pull/1301#discussion_r1936181560
435+
436+
try {
437+
await updateMessage(store.connection,
438+
messageId: someMessageIdInTopic,
439+
topic: _actionIsResolve ? topic.resolve() : topic.unresolve(),
440+
propagateMode: PropagateMode.changeAll,
441+
sendNotificationToOldThread: false,
442+
sendNotificationToNewThread: true,
443+
);
444+
} catch (e) {
445+
if (!pageContext.mounted) return;
446+
447+
String? errorMessage;
448+
switch (e) {
449+
case ZulipApiException():
450+
errorMessage = e.message;
451+
// TODO(#741) specific messages for common errors, like network errors
452+
// (support with reusable code)
453+
default:
454+
}
455+
456+
final title = _actionIsResolve
457+
? zulipLocalizations.errorResolveTopicFailedTitle
458+
: zulipLocalizations.errorUnresolveTopicFailedTitle;
459+
showErrorDialog(context: pageContext, title: title, message: errorMessage);
460+
}
461+
}
462+
}
463+
380464
/// Show a sheet of actions you can take on a message in the message list.
381465
///
382466
/// Must have a [MessageListPage] ancestor.

lib/widgets/inbox.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,8 @@ class _TopicItem extends StatelessWidget {
507507

508508
@override
509509
Widget build(BuildContext context) {
510-
final _StreamSectionTopicData(:topic, :count, :hasMention) = data;
510+
final _StreamSectionTopicData(
511+
:topic, :count, :hasMention, :lastUnreadId) = data;
511512

512513
final store = PerAccountStoreWidget.of(context);
513514
final subscription = store.subscriptions[streamId]!;
@@ -525,7 +526,9 @@ class _TopicItem extends StatelessWidget {
525526
MessageListPage.buildRoute(context: context, narrow: narrow));
526527
},
527528
onLongPress: () => showTopicActionSheet(context,
528-
channelId: streamId, topic: topic),
529+
channelId: streamId,
530+
topic: topic,
531+
someMessageIdInTopic: lastUnreadId),
529532
child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34),
530533
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
531534
const SizedBox(width: 63),

lib/widgets/message_list.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,20 @@ class MessageListAppBarTitle extends StatelessWidget {
404404
width: double.infinity,
405405
child: GestureDetector(
406406
behavior: HitTestBehavior.translucent,
407-
onLongPress: () => showTopicActionSheet(context,
408-
channelId: streamId, topic: topic),
407+
onLongPress: () {
408+
final someMessage = MessageListPage.ancestorOf(context)
409+
.model?.messages.firstOrNull;
410+
// If someMessage is null, the topic action sheet won't have a
411+
// resolve/unresolve button. That seems OK; in that case we're
412+
// either still fetching messages (and the user can reopen the
413+
// sheet after that finishes) or there aren't any messages to
414+
// act on anyway.
415+
assert(someMessage == null || narrow.containsMessage(someMessage));
416+
showTopicActionSheet(context,
417+
channelId: streamId,
418+
topic: topic,
419+
someMessageIdInTopic: someMessage?.id);
420+
},
409421
child: Column(
410422
crossAxisAlignment: willCenterTitle ? CrossAxisAlignment.center
411423
: CrossAxisAlignment.start,
@@ -1127,7 +1139,9 @@ class StreamMessageRecipientHeader extends StatelessWidget {
11271139
MessageListPage.buildRoute(context: context,
11281140
narrow: TopicNarrow.ofMessage(message))),
11291141
onLongPress: () => showTopicActionSheet(context,
1130-
channelId: message.streamId, topic: topic),
1142+
channelId: message.streamId,
1143+
topic: topic,
1144+
someMessageIdInTopic: message.id),
11311145
child: ColoredBox(
11321146
color: backgroundColor,
11331147
child: Row(

0 commit comments

Comments
 (0)