Skip to content

Commit b901863

Browse files
committed
action_sheet: Support muting/unmuting/following topics
Currently, we don't have buttons, like "resolve topic", other than the ones added here. The switch statements follow the layout of the legacy app implementation. See also: https://github.com/zulip/zulip-mobile/blob/715d60a5e87fe37032bce58bd72edb99208e15be/src/action-sheets/index.js#L656-L753 Fixes: #348 Signed-off-by: Zixuan James Li <[email protected]>
1 parent cb28f47 commit b901863

File tree

5 files changed

+524
-1
lines changed

5 files changed

+524
-1
lines changed

assets/l10n/app_en.arb

+32
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@
4343
"@permissionsDeniedReadExternalStorage": {
4444
"description": "Message for dialog asking the user to grant permissions for external storage read access."
4545
},
46+
"actionSheetOptionMuteTopic": "Mute topic",
47+
"@actionSheetOptionMuteTopic": {
48+
"description": "Label for muting a topic on action sheet."
49+
},
50+
"actionSheetOptionUnmuteTopic": "Unmute topic",
51+
"@actionSheetOptionUnmuteTopic": {
52+
"description": "Label for unmuting a topic on action sheet."
53+
},
54+
"actionSheetOptionFollowTopic": "Follow topic",
55+
"@actionSheetOptionFollowTopic": {
56+
"description": "Label for following a topic on action sheet."
57+
},
58+
"actionSheetOptionUnfollowTopic": "Unfollow topic",
59+
"@actionSheetOptionUnfollowTopic": {
60+
"description": "Label for unfollowing a topic on action sheet."
61+
},
4662
"actionSheetOptionCopyMessageText": "Copy message text",
4763
"@actionSheetOptionCopyMessageText": {
4864
"description": "Label for copy message text button on action sheet."
@@ -172,6 +188,22 @@
172188
"error": {"type": "String", "example": "Invalid format"}
173189
}
174190
},
191+
"errorMuteTopicFailed": "Failed to mute topic",
192+
"@errorMuteTopicFailed": {
193+
"description": "Error message when muting a topic failed."
194+
},
195+
"errorUnmuteTopicFailed": "Failed to unmute topic",
196+
"@errorUnmuteTopicFailed": {
197+
"description": "Error message when unmuting a topic failed."
198+
},
199+
"errorFollowTopicFailed": "Failed to follow topic",
200+
"@errorFollowTopicFailed": {
201+
"description": "Error message when following a topic failed."
202+
},
203+
"errorUnfollowTopicFailed": "Failed to unfollow topic",
204+
"@errorUnfollowTopicFailed": {
205+
"description": "Error message when unfollowing a topic failed."
206+
},
175207
"errorSharingFailed": "Sharing failed",
176208
"@errorSharingFailed": {
177209
"description": "Error message when sharing a message failed."

lib/widgets/action_sheet.dart

+194
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart';
88

99
import '../api/exception.dart';
1010
import '../api/model/model.dart';
11+
import '../api/route/channels.dart';
1112
import '../api/route/messages.dart';
1213
import '../model/internal_link.dart';
1314
import '../model/narrow.dart';
@@ -140,6 +141,199 @@ class ActionSheetCancelButton extends StatelessWidget {
140141
}
141142
}
142143

144+
/// Show a sheet of actions you can take on a topic.
145+
void showTopicActionSheet(BuildContext context, {
146+
required int channelId,
147+
required String topic,
148+
}) {
149+
final narrow = TopicNarrow(channelId, topic);
150+
UserTopicUpdateButton button({
151+
UserTopicVisibilityPolicy? from,
152+
required UserTopicVisibilityPolicy to,
153+
}) {
154+
return UserTopicUpdateButton(
155+
currentVisibilityPolicy: from,
156+
newVisibilityPolicy: to,
157+
narrow: narrow,
158+
pageContext: context);
159+
}
160+
161+
final mute = button(to: UserTopicVisibilityPolicy.muted);
162+
final unmute = button(from: UserTopicVisibilityPolicy.muted,
163+
to: UserTopicVisibilityPolicy.none);
164+
final unmuteInMutedChannel = button(to: UserTopicVisibilityPolicy.unmuted);
165+
final follow = button(to: UserTopicVisibilityPolicy.followed);
166+
final unfollow = button(from: UserTopicVisibilityPolicy.followed,
167+
to: UserTopicVisibilityPolicy.none);
168+
169+
final store = PerAccountStoreWidget.of(context);
170+
final channelMuted = store.subscriptions[channelId]?.isMuted;
171+
final visibilityPolicy = store.topicVisibilityPolicy(channelId, topic);
172+
173+
// TODO(server-7): simplify this condition away
174+
final supportsUnmutingTopics = store.connection.zulipFeatureLevel! >= 170;
175+
// TODO(server-8): simplify this condition away
176+
final supportsFollowingTopics = store.connection.zulipFeatureLevel! >= 219;
177+
178+
final optionButtons = <Widget>[];
179+
if (channelMuted != null && !channelMuted) {
180+
switch (visibilityPolicy) {
181+
case UserTopicVisibilityPolicy.muted:
182+
optionButtons.add(unmute);
183+
if (supportsFollowingTopics) {
184+
optionButtons.add(follow);
185+
}
186+
case UserTopicVisibilityPolicy.none:
187+
case UserTopicVisibilityPolicy.unmuted:
188+
optionButtons.add(mute);
189+
if (supportsFollowingTopics) {
190+
optionButtons.add(follow);
191+
}
192+
case UserTopicVisibilityPolicy.followed:
193+
optionButtons.add(mute);
194+
if (supportsFollowingTopics) {
195+
optionButtons.add(unfollow);
196+
}
197+
case UserTopicVisibilityPolicy.unknown:
198+
assert(false);
199+
return;
200+
}
201+
} else if (channelMuted != null && channelMuted) {
202+
if (supportsUnmutingTopics) {
203+
switch (visibilityPolicy) {
204+
case UserTopicVisibilityPolicy.none:
205+
case UserTopicVisibilityPolicy.muted:
206+
optionButtons.add(unmuteInMutedChannel);
207+
if (supportsFollowingTopics) {
208+
optionButtons.add(follow);
209+
}
210+
case UserTopicVisibilityPolicy.unmuted:
211+
optionButtons.add(mute);
212+
if (supportsFollowingTopics) {
213+
optionButtons.add(follow);
214+
}
215+
case UserTopicVisibilityPolicy.followed:
216+
optionButtons.add(mute);
217+
if (supportsFollowingTopics) {
218+
optionButtons.add(unfollow);
219+
}
220+
case UserTopicVisibilityPolicy.unknown:
221+
assert(false);
222+
return;
223+
}
224+
}
225+
} else {
226+
// Not subscribed to the channel; there is no user topic change to be made.
227+
return;
228+
}
229+
230+
if (optionButtons.isEmpty) {
231+
assert(!supportsUnmutingTopics);
232+
return;
233+
}
234+
235+
_showActionSheet(context, optionButtons: optionButtons);
236+
}
237+
238+
class UserTopicUpdateButton extends ActionSheetMenuItemButton {
239+
const UserTopicUpdateButton({
240+
super.key,
241+
this.currentVisibilityPolicy,
242+
required this.newVisibilityPolicy,
243+
required this.narrow,
244+
required super.pageContext,
245+
});
246+
247+
final UserTopicVisibilityPolicy? currentVisibilityPolicy;
248+
final UserTopicVisibilityPolicy newVisibilityPolicy;
249+
final TopicNarrow narrow;
250+
251+
@override IconData get icon {
252+
switch (newVisibilityPolicy) {
253+
case UserTopicVisibilityPolicy.none:
254+
return ZulipIcons.inherit;
255+
case UserTopicVisibilityPolicy.muted:
256+
return ZulipIcons.mute;
257+
case UserTopicVisibilityPolicy.unmuted:
258+
return ZulipIcons.unmute;
259+
case UserTopicVisibilityPolicy.followed:
260+
return ZulipIcons.follow;
261+
case UserTopicVisibilityPolicy.unknown:
262+
assert(false);
263+
return ZulipIcons.inherit;
264+
}
265+
}
266+
267+
@override
268+
String label(ZulipLocalizations zulipLocalizations) {
269+
switch ((currentVisibilityPolicy, newVisibilityPolicy)) {
270+
case (UserTopicVisibilityPolicy.muted, UserTopicVisibilityPolicy.none):
271+
return zulipLocalizations.actionSheetOptionUnmuteTopic;
272+
case (UserTopicVisibilityPolicy.followed, UserTopicVisibilityPolicy.none):
273+
return zulipLocalizations.actionSheetOptionUnfollowTopic;
274+
275+
case (_, UserTopicVisibilityPolicy.muted):
276+
return zulipLocalizations.actionSheetOptionMuteTopic;
277+
case (_, UserTopicVisibilityPolicy.unmuted):
278+
return zulipLocalizations.actionSheetOptionUnmuteTopic;
279+
case (_, UserTopicVisibilityPolicy.followed):
280+
return zulipLocalizations.actionSheetOptionFollowTopic;
281+
282+
case (_, UserTopicVisibilityPolicy.none):
283+
case (_, UserTopicVisibilityPolicy.unknown):
284+
assert(false);
285+
return '';
286+
}
287+
}
288+
289+
String _errorTitle(ZulipLocalizations zulipLocalizations) {
290+
switch ((currentVisibilityPolicy, newVisibilityPolicy)) {
291+
case (UserTopicVisibilityPolicy.muted, UserTopicVisibilityPolicy.none):
292+
return zulipLocalizations.errorUnmuteTopicFailed;
293+
case (UserTopicVisibilityPolicy.followed, UserTopicVisibilityPolicy.none):
294+
return zulipLocalizations.errorUnfollowTopicFailed;
295+
296+
case (_, UserTopicVisibilityPolicy.muted):
297+
return zulipLocalizations.errorMuteTopicFailed;
298+
case (_, UserTopicVisibilityPolicy.unmuted):
299+
return zulipLocalizations.errorUnmuteTopicFailed;
300+
case (_, UserTopicVisibilityPolicy.followed):
301+
return zulipLocalizations.errorFollowTopicFailed;
302+
303+
case (_, UserTopicVisibilityPolicy.none):
304+
case (_, UserTopicVisibilityPolicy.unknown):
305+
assert(false);
306+
return '';
307+
}
308+
}
309+
310+
@override void onPressed() async {
311+
try {
312+
await updateUserTopic(
313+
PerAccountStoreWidget.of(pageContext).connection,
314+
streamId: narrow.streamId,
315+
topic: narrow.topic,
316+
visibilityPolicy: newVisibilityPolicy);
317+
} catch (e) {
318+
if (!pageContext.mounted) return;
319+
320+
String? errorMessage;
321+
322+
switch (e) {
323+
case ZulipApiException():
324+
errorMessage = e.message;
325+
// TODO(#741) specific messages for common errors, like network errors
326+
// (support with reusable code)
327+
default:
328+
}
329+
330+
final zulipLocalizations = ZulipLocalizations.of(pageContext);
331+
showErrorDialog(context: pageContext,
332+
title: _errorTitle(zulipLocalizations), message: errorMessage);
333+
}
334+
}
335+
}
336+
143337
/// Show a sheet of actions you can take on a message in the message list.
144338
///
145339
/// Must have a [MessageListPage] ancestor.

lib/widgets/inbox.dart

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../api/model/model.dart';
44
import '../model/narrow.dart';
55
import '../model/recent_dm_conversations.dart';
66
import '../model/unreads.dart';
7+
import 'action_sheet.dart';
78
import 'app_bar.dart';
89
import 'icons.dart';
910
import 'message_list.dart';
@@ -514,6 +515,8 @@ class _TopicItem extends StatelessWidget {
514515
Navigator.push(context,
515516
MessageListPage.buildRoute(context: context, narrow: narrow));
516517
},
518+
onLongPress: () => showTopicActionSheet(context,
519+
channelId: streamId, topic: topic),
517520
child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34),
518521
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
519522
const SizedBox(width: 63),

lib/widgets/message_list.dart

+14-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ class MessageListAppBarTitle extends StatelessWidget {
305305
}) {
306306
// A null [Icon.icon] makes a blank space.
307307
final icon = (stream != null) ? iconDataForStream(stream) : null;
308-
return Row(
308+
final appBar = Row(
309309
mainAxisSize: MainAxisSize.min,
310310
// TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc.
311311
// For screenshots of some experiments, see:
@@ -316,6 +316,17 @@ class MessageListAppBarTitle extends StatelessWidget {
316316
const SizedBox(width: 4),
317317
Flexible(child: Text(text)),
318318
]);
319+
320+
if (narrow case TopicNarrow(:final streamId, :final topic)) {
321+
return SizedBox(
322+
width: double.infinity,
323+
child: GestureDetector(
324+
onLongPress: () => showTopicActionSheet(context,
325+
channelId: streamId, topic: topic),
326+
child: appBar));
327+
}
328+
329+
return appBar;
319330
}
320331

321332
@override
@@ -1016,6 +1027,8 @@ class StreamMessageRecipientHeader extends StatelessWidget {
10161027
onTap: () => Navigator.push(context,
10171028
MessageListPage.buildRoute(context: context,
10181029
narrow: TopicNarrow.ofMessage(message))),
1030+
onLongPress: () => showTopicActionSheet(context,
1031+
channelId: message.streamId, topic: topic),
10191032
child: ColoredBox(
10201033
color: backgroundColor,
10211034
child: Row(

0 commit comments

Comments
 (0)