Skip to content

Commit

Permalink
action_sheet: Support muting/unmuting/following topics
Browse files Browse the repository at this point in the history
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: zulip#348

Signed-off-by: Zixuan James Li <[email protected]>
  • Loading branch information
PIG208 committed Nov 9, 2024
1 parent 798f772 commit 4223fc4
Show file tree
Hide file tree
Showing 5 changed files with 524 additions and 1 deletion.
32 changes: 32 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@
"@permissionsDeniedReadExternalStorage": {
"description": "Message for dialog asking the user to grant permissions for external storage read access."
},
"actionSheetOptionMuteTopic": "Mute topic",
"@actionSheetOptionMuteTopic": {
"description": "Label for muting a topic on action sheet."
},
"actionSheetOptionUnmuteTopic": "Unmute topic",
"@actionSheetOptionUnmuteTopic": {
"description": "Label for unmuting a topic on action sheet."
},
"actionSheetOptionFollowTopic": "Follow topic",
"@actionSheetOptionFollowTopic": {
"description": "Label for following a topic on action sheet."
},
"actionSheetOptionUnfollowTopic": "Unfollow topic",
"@actionSheetOptionUnfollowTopic": {
"description": "Label for unfollowing a topic on action sheet."
},
"actionSheetOptionCopyMessageText": "Copy message text",
"@actionSheetOptionCopyMessageText": {
"description": "Label for copy message text button on action sheet."
Expand Down Expand Up @@ -172,6 +188,22 @@
"error": {"type": "String", "example": "Invalid format"}
}
},
"errorMuteTopicFailed": "Failed to mute topic",
"@errorMuteTopicFailed": {
"description": "Error message when muting a topic failed."
},
"errorUnmuteTopicFailed": "Failed to unmute topic",
"@errorUnmuteTopicFailed": {
"description": "Error message when unmuting a topic failed."
},
"errorFollowTopicFailed": "Failed to follow topic",
"@errorFollowTopicFailed": {
"description": "Error message when following a topic failed."
},
"errorUnfollowTopicFailed": "Failed to unfollow topic",
"@errorUnfollowTopicFailed": {
"description": "Error message when unfollowing a topic failed."
},
"errorSharingFailed": "Sharing failed",
"@errorSharingFailed": {
"description": "Error message when sharing a message failed."
Expand Down
194 changes: 194 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart';

import '../api/exception.dart';
import '../api/model/model.dart';
import '../api/route/channels.dart';
import '../api/route/messages.dart';
import '../model/internal_link.dart';
import '../model/narrow.dart';
Expand Down Expand Up @@ -140,6 +141,199 @@ class ActionSheetCancelButton extends StatelessWidget {
}
}

/// Show a sheet of actions you can take on a topic.
void showTopicActionSheet(BuildContext context, {
required int channelId,
required String topic,
}) {
final narrow = TopicNarrow(channelId, topic);
UserTopicUpdateButton button({
UserTopicVisibilityPolicy? from,
required UserTopicVisibilityPolicy to,
}) {
return UserTopicUpdateButton(
currentVisibilityPolicy: from,
newVisibilityPolicy: to,
narrow: narrow,
pageContext: context);
}

final mute = button(to: UserTopicVisibilityPolicy.muted);
final unmute = button(from: UserTopicVisibilityPolicy.muted,
to: UserTopicVisibilityPolicy.none);
final unmuteInMutedChannel = button(to: UserTopicVisibilityPolicy.unmuted);
final follow = button(to: UserTopicVisibilityPolicy.followed);
final unfollow = button(from: UserTopicVisibilityPolicy.followed,
to: UserTopicVisibilityPolicy.none);

final store = PerAccountStoreWidget.of(context);
final channelMuted = store.subscriptions[channelId]?.isMuted;
final visibilityPolicy = store.topicVisibilityPolicy(channelId, topic);

// TODO(server-7): simplify this condition away
final supportsUnmutingTopics = store.connection.zulipFeatureLevel! >= 170;
// TODO(server-8): simplify this condition away
final supportsFollowingTopics = store.connection.zulipFeatureLevel! >= 219;

final optionButtons = <Widget>[];
if (channelMuted != null && !channelMuted) {
switch (visibilityPolicy) {
case UserTopicVisibilityPolicy.muted:
optionButtons.add(unmute);
if (supportsFollowingTopics) {
optionButtons.add(follow);
}
case UserTopicVisibilityPolicy.none:
case UserTopicVisibilityPolicy.unmuted:
optionButtons.add(mute);
if (supportsFollowingTopics) {
optionButtons.add(follow);
}
case UserTopicVisibilityPolicy.followed:
optionButtons.add(mute);
if (supportsFollowingTopics) {
optionButtons.add(unfollow);
}
case UserTopicVisibilityPolicy.unknown:
assert(false);
return;
}
} else if (channelMuted != null && channelMuted) {
if (supportsUnmutingTopics) {
switch (visibilityPolicy) {
case UserTopicVisibilityPolicy.none:
case UserTopicVisibilityPolicy.muted:
optionButtons.add(unmuteInMutedChannel);
if (supportsFollowingTopics) {
optionButtons.add(follow);
}
case UserTopicVisibilityPolicy.unmuted:
optionButtons.add(mute);
if (supportsFollowingTopics) {
optionButtons.add(follow);
}
case UserTopicVisibilityPolicy.followed:
optionButtons.add(mute);
if (supportsFollowingTopics) {
optionButtons.add(unfollow);
}
case UserTopicVisibilityPolicy.unknown:
assert(false);
return;
}
}
} else {
// Not subscribed to the channel; there is no user topic change to be made.
return;
}

if (optionButtons.isEmpty) {
assert(!supportsUnmutingTopics);
return;
}

_showActionSheet(context, optionButtons: optionButtons);
}

class UserTopicUpdateButton extends ActionSheetMenuItemButton {
const UserTopicUpdateButton({
super.key,
this.currentVisibilityPolicy,
required this.newVisibilityPolicy,
required this.narrow,
required super.pageContext,
});

final UserTopicVisibilityPolicy? currentVisibilityPolicy;
final UserTopicVisibilityPolicy newVisibilityPolicy;
final TopicNarrow narrow;

@override IconData get icon {
switch (newVisibilityPolicy) {
case UserTopicVisibilityPolicy.none:
return ZulipIcons.inherit;
case UserTopicVisibilityPolicy.muted:
return ZulipIcons.mute;
case UserTopicVisibilityPolicy.unmuted:
return ZulipIcons.unmute;
case UserTopicVisibilityPolicy.followed:
return ZulipIcons.follow;
case UserTopicVisibilityPolicy.unknown:
assert(false);
return ZulipIcons.inherit;
}
}

@override
String label(ZulipLocalizations zulipLocalizations) {
switch ((currentVisibilityPolicy, newVisibilityPolicy)) {
case (UserTopicVisibilityPolicy.muted, UserTopicVisibilityPolicy.none):
return zulipLocalizations.actionSheetOptionUnmuteTopic;
case (UserTopicVisibilityPolicy.followed, UserTopicVisibilityPolicy.none):
return zulipLocalizations.actionSheetOptionUnfollowTopic;

case (_, UserTopicVisibilityPolicy.muted):
return zulipLocalizations.actionSheetOptionMuteTopic;
case (_, UserTopicVisibilityPolicy.unmuted):
return zulipLocalizations.actionSheetOptionUnmuteTopic;
case (_, UserTopicVisibilityPolicy.followed):
return zulipLocalizations.actionSheetOptionFollowTopic;

case (_, UserTopicVisibilityPolicy.none):
case (_, UserTopicVisibilityPolicy.unknown):
assert(false);
return '';
}
}

String _errorTitle(ZulipLocalizations zulipLocalizations) {
switch ((currentVisibilityPolicy, newVisibilityPolicy)) {
case (UserTopicVisibilityPolicy.muted, UserTopicVisibilityPolicy.none):
return zulipLocalizations.errorUnmuteTopicFailed;
case (UserTopicVisibilityPolicy.followed, UserTopicVisibilityPolicy.none):
return zulipLocalizations.errorUnfollowTopicFailed;

case (_, UserTopicVisibilityPolicy.muted):
return zulipLocalizations.errorMuteTopicFailed;
case (_, UserTopicVisibilityPolicy.unmuted):
return zulipLocalizations.errorUnmuteTopicFailed;
case (_, UserTopicVisibilityPolicy.followed):
return zulipLocalizations.errorFollowTopicFailed;

case (_, UserTopicVisibilityPolicy.none):
case (_, UserTopicVisibilityPolicy.unknown):
assert(false);
return '';
}
}

@override void onPressed() async {
try {
await updateUserTopic(
PerAccountStoreWidget.of(pageContext).connection,
streamId: narrow.streamId,
topic: narrow.topic,
visibilityPolicy: newVisibilityPolicy);
} catch (e) {
if (!pageContext.mounted) return;

String? errorMessage;

switch (e) {
case ZulipApiException():
errorMessage = e.message;
// TODO(#741) specific messages for common errors, like network errors
// (support with reusable code)
default:
}

final zulipLocalizations = ZulipLocalizations.of(pageContext);
showErrorDialog(context: pageContext,
title: _errorTitle(zulipLocalizations), message: errorMessage);
}
}
}

/// Show a sheet of actions you can take on a message in the message list.
///
/// Must have a [MessageListPage] ancestor.
Expand Down
3 changes: 3 additions & 0 deletions lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '../api/model/model.dart';
import '../model/narrow.dart';
import '../model/recent_dm_conversations.dart';
import '../model/unreads.dart';
import 'action_sheet.dart';
import 'app_bar.dart';
import 'icons.dart';
import 'message_list.dart';
Expand Down Expand Up @@ -514,6 +515,8 @@ class _TopicItem extends StatelessWidget {
Navigator.push(context,
MessageListPage.buildRoute(context: context, narrow: narrow));
},
onLongPress: () => showTopicActionSheet(context,
channelId: streamId, topic: topic),
child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
const SizedBox(width: 63),
Expand Down
15 changes: 14 additions & 1 deletion lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class MessageListAppBarTitle extends StatelessWidget {
}) {
// A null [Icon.icon] makes a blank space.
final icon = (stream != null) ? iconDataForStream(stream) : null;
return Row(
final appBar = Row(
mainAxisSize: MainAxisSize.min,
// TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc.
// For screenshots of some experiments, see:
Expand All @@ -316,6 +316,17 @@ class MessageListAppBarTitle extends StatelessWidget {
const SizedBox(width: 4),
Flexible(child: Text(text)),
]);

if (narrow case TopicNarrow(:final streamId, :final topic)) {
return SizedBox(
width: double.infinity,
child: GestureDetector(
onLongPress: () => showTopicActionSheet(context,
channelId: streamId, topic: topic),
child: appBar));
}

return appBar;
}

@override
Expand Down Expand Up @@ -1016,6 +1027,8 @@ class StreamMessageRecipientHeader extends StatelessWidget {
onTap: () => Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: TopicNarrow.ofMessage(message))),
onLongPress: () => showTopicActionSheet(context,
channelId: message.streamId, topic: topic),
child: ColoredBox(
color: backgroundColor,
child: Row(
Expand Down
Loading

0 comments on commit 4223fc4

Please sign in to comment.