Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support topic muting/unmuting/following #1041

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented Nov 1, 2024

Screenshots
channel topic screenshot
unmuted inherit 1000015318
unmuted muted 1000015319
muted inherit 1000015315
muted followed 1000015317
muted unmuted 1000015316

Reference zulip-mobile implementation: https://github.com/zulip/zulip-mobile/blob/715d60a5e87fe37032bce58bd72edb99208e15be/src/action-sheets/index.js#L656-L753

Fixes: #348

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Quick initial comments from skimming this draft.

@override void onPressed(BuildContext context) async {
Navigator.of(context).pop();
try {
await updateUserTopic(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems more specific than the name TopicActionSheetButton reflects. We'll have other options in the topic action sheet that aren't about visibility policy, like resolve/unresolve.

It makes sense to group all these options under a common base class; it should just get a more specific name.


final int streamId;
final String topic;
final BuildContext topicParentContext;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name doesn't feel right conceptually. I'll see about sending a quick PR refactoring the existing message-action-sheet options that might help clarify what this field is about.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sent #1044.

@PIG208 PIG208 force-pushed the pr-muting branch 8 times, most recently from c21f055 to be64a22 Compare November 7, 2024 22:53
@PIG208 PIG208 marked this pull request as ready for review November 7, 2024 22:53
@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label Nov 7, 2024
@PIG208 PIG208 force-pushed the pr-muting branch 3 times, most recently from 0b6cdac to 11209cc Compare November 21, 2024 19:06
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below, and I agree with your comment at #348 (comment) 🙂

I think a design requirement of this feature is to also display mute/following states for each topic. At this point we can borrow that from the legacy mobile app.


void _showActionSheet(
BuildContext context, {
required List<Widget> optionButtons,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
required List<Widget> optionButtons,
required List<ActionSheetMenuItemButton> optionButtons,

Comment on lines 147 to 367
void showMessageActionSheet({required BuildContext context, required Message message}) {
void showMessageActionSheet(BuildContext context, {required Message message}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action_sheet [nfc]: Make context an unamed parameter

nit: "unnamed" (spelling); also, is there room in the summary line to name the function whose params we're talking about? I guess there's just one other function in the action_sheet subsystem with a named context param, fetchRawContentWithFeedback. How about:

action_sheet [nfc]: Make showMessageActionSheet's context an unnamed param

Comment on lines 57 to 59
// There can be an error when muting a topic that is already muted
// or unmuting one that is already unmuted. Let it throw because we can't
// reliably filter out the error, which doesn't have a specific "code".
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear from the comment that this would be the right layer to "filter out" such errors even if we could; do we need the comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this isn't the right layer to handle the error. Since we are not planning to catch that anyway before we drop this fallback, calling this out doesn't really do anything helpful. I will just move this to the commit message.

// There can be an error when muting a topic that is already muted
// or unmuting one that is already unmuted. Let it throw because we can't
// reliably filter out the error, which doesn't have a specific "code".
return connection.post('muteTopic', (_) {}, 'users/me/subscriptions/muted_topics', {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be connection.patch: https://zulip.com/api/mute-topic

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good catch! Looks like we don't even have the connection.patch method; will add it in a prep commit.

@@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appBar doesn't sound like the right name for something that can get returned from a function named _buildStreamRow. How about just result?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the name _buildStreamRow seems like a hint that it's not really the right place for something that opens a topic action sheet. I think we want a comment in the if (narrow case TopicNarrow that the added GestureDetector will go somewhere else or be removed as part of #1039.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While updating this PR to support message list app bar with two rows, I found a better place where this GestureDetector belongs to:

      case TopicNarrow(:var streamId, :var topic):
        final store = PerAccountStoreWidget.of(context);
        final stream = store.streams[streamId];

        return SizedBox(
          width: double.infinity,
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onLongPress: () => showTopicActionSheet(context,
              channelId: streamId, topic: topic),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildStreamRow(context, stream: stream),
                _buildTopicRow(context, stream: stream, topic: topic),
              ])));

to: UserTopicVisibilityPolicy.none);

Future<void> setupToTopicActionSheet(WidgetTester tester, {
required bool? isChannelMuted,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be more explicit about what null means for isChannelMuted. I think it's supposed to mean that the self-user isn't subscribed to the channel (right?), but that isn't clear from test-failure output—

example
$ flutter test test/widgets/action_sheet_test.dart 
00:02 +17: UserTopicUpdateButton check expected buttons null UserTopicVisibilityPolicy.none                                                 
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: a Finder that:
Actual: means none were found but one was expected
Which: exactly one matching candidate

When the exception was thrown, this was the stack:
#0      check.<anonymous closure> (package:checks/src/checks.dart:85:9)
#1      _TestContext.expect (package:checks/src/checks.dart:708:12)
#2      LegacyMatcher.legacyMatcher (package:legacy_checks/src/matcher_compat.dart:27:13)
#3      FinderBaseChecks.findsOne (package:flutter_checks/src/flutter_checks.dart:187:5)
#4      main.<anonymous closure>.checkButtons (file:///Users/chrisbobbe/dev/zulip-flutter/test/widgets/action_sheet_test.dart:225:39)
#5      main.<anonymous closure>.<anonymous closure>.<anonymous closure> (file:///Users/chrisbobbe/dev/zulip-flutter/test/widgets/action_sheet_test.dart:322:11)
<asynchronous suspension>
#6      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#7      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1027:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

The test description was:
  null UserTopicVisibilityPolicy.none
════════════════════════════════════════════════════════════════════════════════════════════════════
00:02 +17 -1: UserTopicUpdateButton check expected buttons null UserTopicVisibilityPolicy.none [E]                                          
  Test failed. See exception logs above.
  The test description was: null UserTopicVisibilityPolicy.none

or from reading the tests, except if you dig into the implementation of setupToTopicActionSheet. A minimal fix would be to give this function a dartdoc and update the test descriptions where they currently just say $isChannelMuted.

Copy link
Member Author

@PIG208 PIG208 Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be more explicit about what null means for isChannelMuted. I think it's supposed to mean that the self-user isn't subscribed to the channel (right?), but that isn't clear from test-failure output—

Yeah. Added dartdoc, updated the test description and left the topic names as-is, because the description is what shows up when a test fails.

Comment on lines 221 to 223
case UserTopicVisibilityPolicy.unknown:
assert(false);
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this case be reached? It seems like we should either:

  • Reject unrecognized policy values at the edge as malformed server data, or
  • Handle them gracefully in the app, e.g., not handle them by giving up on showing the topic action sheet, which might still have useful buttons like "move topic" or "resolve topic" etc. once we implement those

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. By having this unknown value in the enum we're already paying most of the cost of gracefully handling the situation where the server introduces a new policy value, so we should handle it here too.

Copy link
Member Author

@PIG208 PIG208 Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not expect it here because we already semi-explicitly keep it out of our data structure, so it would be a bug to get unknown here. However, I think it is not obvious that we are doing so because the enum always includes unknown.

static bool _warnInvalidVisibilityPolicy(UserTopicVisibilityPolicy visibilityPolicy) {
if (visibilityPolicy == UserTopicVisibilityPolicy.unknown) {
// Not a value we expect. Keep it out of our data structures. // TODO(log)
return true;
}
return false;
}

if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {
visibilityPolicy = UserTopicVisibilityPolicy.none;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, indeed. In that case it doesn't matter what this line does — though it's still probably cleaner to have it skip this type of button rather than abort the whole function. And it should have a comment saying why it's impossible.

I've noticed in a few other places that it'd be good to have a type for "visibility policy, but only the known/valid ones". Probably that points to removing unknown from the enum, and using a nullable type where we actually want that value. Out of scope for this PR, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this will allow us to remove the cases here (where this new assert(false) originally comes from). It is good to have the type confirm that we process the unknown values at the edge.

Copy link
Member Author

@PIG208 PIG208 Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #1074 and included TODO comments linked in this PR. (Also removed assert(false)'s under UserTopicVisibilityPolicy.none because it was incorrectly grouped with UserTopicVisibilityPolicy.unknown)

Comment on lines 227 to 228
// Not subscribed to the channel; there is no user topic change to be made.
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment helps understand what this case is about, but I think I would remove the return.

Then, we can add chunks of code for more buttons in the future, like copy-link-to-topic, without having to go back and think about this other chunk of code that mostly looks like it's about different buttons.

}

if (optionButtons.isEmpty) {
assert(!supportsUnmutingTopics);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need this assert; it should be redundant with tests, and it doesn't unlock any opportunities to simplify code that comes after it.

Comment on lines 231 to 240
if (optionButtons.isEmpty) {
assert(!supportsUnmutingTopics);
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we return before showing the action sheet, this long-press handler is a no-op. Screen reader software can't tell it's a no-op, so I think it still presents the element as though it offers a long-press interaction, which isn't really accurate.

So for accessibility, what we'd normally want is to pass null to the GestureDetector instead of a no-op function. Alternatively we could design an "empty" appearance for the action sheet.

Those solutions could be finicky or take some time—probably our solution is to eventually just remove the case where the action sheet has no buttons, by implementing a button that's present unconditionally. So for now let's just leave a TODO(a11y) for that. (I think "Copy link to topic" would be such a button.)

@PIG208
Copy link
Member Author

PIG208 commented Nov 22, 2024

Thanks for the review! Updated the PR.

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below, including some things I missed last time (oops).

|| visibilityPolicy == UserTopicVisibilityPolicy.muted);
final op = visibilityPolicy == UserTopicVisibilityPolicy.none ? 'remove'
: 'add';
return connection.patch('muteTopic', (_) {}, 'users/me/subscriptions/muted_topics', {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api: Add route updateUserTopic

For the legacy case, there can be an error when muting a topic that
is already muted or unmuting one that is already unmuted.  Let it
throw because we can't reliably filter out the error, which doesn't
have a specific "code".

Signed-off-by: Zixuan James Li <[email protected]>

Moving this from a code comment to the commit message doesn't make it more convincing 🙂—as I understand it, the reason we don't catch the error is the same reason we don't generally catch API errors in the binding layer: we want the bindings to correspond as closely as possible to the documented API, as a thin wrapper, so they don't hide or mess with things that callers might be interested in.

I see just one catch in lib/api/route—

/// Convenience function to get a single message from any server.
///
/// This encapsulates a server-feature check.
///
/// Gives null if the server reports that the message doesn't exist.
// TODO(server-5) Simplify this away; just use getMessage.
Future<Message?> getMessageCompat(ApiConnection connection, {
  required int messageId,
  bool? applyMarkdown,
}) async {
  final useLegacyApi = connection.zulipFeatureLevel! < 120;
  if (useLegacyApi) {
    final response = await getMessages(connection,
      narrow: [ApiNarrowMessageId(messageId)],
      anchor: NumericAnchor(messageId),
      numBefore: 0,
      numAfter: 0,
      applyMarkdown: applyMarkdown,

      // Hard-code this param to `true`, as the new single-message API
      // effectively does:
      //   https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60client_gravatar.60.20in.20.60messages.2F.7Bmessage_id.7D.60/near/1418337
      clientGravatar: true,
    );
    return response.messages.firstOrNull;
  } else {
    try {
      final response = await getMessage(connection,
        messageId: messageId,
        applyMarkdown: applyMarkdown,
      );
      return response.message;
    } on ZulipApiException catch (e) {
      if (e.code == 'BAD_REQUEST') {
        // Servers use this code when the message doesn't exist, according to
        // the example in the doc.
        return null;
      }
      rethrow;
    }
  }
}

In that case we have an explicitly thicker wrapper (marked "compat") that encapsulates a difference between legacy and current behavior, so callers don't have to think about the two behaviors separately. (Because of that catch, callers don't need both a null check and their own catch.)

That kind of encapsulation might actually be a fine reason to want to "filter out" these errors in this binding layer, I'm not sure. But if that's the reason, it's not clear from your paragraph about it. Are the errors useful on those legacy servers, or are they basically just an API wart? Let's write an opinion on that first, if we're going to talk about dropping things from the server, instead of just saying we don't drop things because we can't drop them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. The story is that the caller of this is not expected to handle the error, mostly because it isn't helpful to the user. If the topic is already muted/unmuted, and that the user wants it to be muted/unmuted, respectively, there really isn't anything that requires actions from the user:

     api: Add route updateUserTopic

     For the legacy case, there can be an error when muting a topic that
-    is already muted or unmuting one that is already unmuted.  Let it
-    throw because we can't reliably filter out the error, which doesn't
-    have a specific "code".
+    is already muted or unmuting one that is already unmuted.
+
+    The callers are not expected to handle such errors because they aren't
+    really actionable.

Comment on lines +198 to +201
case UserTopicVisibilityPolicy.unknown:
// TODO(#1074): This should be unreachable as we keep `unknown` out of
// our data structures.
assert(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case UserTopicVisibilityPolicy.unknown:
// TODO(#1074): This should be unreachable as we keep `unknown` out of
// our data structures.
assert(false);
case UserTopicVisibilityPolicy.unknown:
// TODO(#1074): This should be unreachable as we keep `unknown` out of
// our data structures.
assert(false);

At first I read "TODO" and "This should be unreachable" as saying that the case is currently reachable and that we plan to make it unreachable in #1074. In reality, it's currently unreachable (or we expect so, anyway), and we want the case to disappear in #1074. Is that correct?

So I think we'll want

  // TODO(#1074) remove this
  unknown(apiValue: null);

in the UserTopicVisibilityPolicy definition, right? Then when we do that TODO, these unknown cases will fall away naturally as part of that (the analyzer will help us there), so they don't need their own separate TODOs. How about:

      case UserTopicVisibilityPolicy.unknown:
        // This case is unreachable (or should be) because we keep `unknown` out
        // of our data structures. We plan to remove the `unknown` case in #1074.
        assert(false);

Comment on lines 232 to 240
if (optionButtons.isEmpty) {
// TODO(a11y): While long press has no effect when we return early without
// bringing up the action sheet, the screen readers do not have a way to
// know that. However, we may return this early return after we add a
// always-present button.
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be clearer about how the bug affects users? How about:

  if (optionButtons.isEmpty) {
    // TODO(a11y): This case makes a no-op gesture handler; as a consequence,
    //   we're presenting some UI (to people who use screen-reader software) as
    //   though it offers a gesture interaction that it doesn't meaningfully
    //   offer, which is confusing. The solution here is probably to remove this
    //   is-empty case by having at least one button that's always present,
    //   such as "copy link to topic".
    return;
  }

Comment on lines 289 to 297
case (_, UserTopicVisibilityPolicy.none):
return '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A label method returning the empty string looks like a code smell to me; that can't be a helpful label for anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turned out that this would be better as a separate case and keep the assert(false). Added an explanation below:

      case (_, UserTopicVisibilityPolicy.none):
        // This is unexpected because `UserTopicVisibilityPolicy.muted` and
        // `UserTopicVisibilityPolicy.followed` (handled in separate `case`'s)
        // are the only expected `currentVisibilityPolicy`
        // when `newVisibilityPolicy` is `UserTopicVisibilityPolicy.none`.
        assert(false);
        return '';

Comment on lines +162 to +168
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach in this PR—which I understand follows zulip-mobile—doesn't support all the state transitions that web supports. Maybe we can open a followup issue for the unsupported ones?

Here's what web does:

In a muted channel, the menu for a topic lets you choose one of four options: "Mute", "Default", "Unmute", "Follow":

image

In an unmuted channel, the menu for a topic lets you choose one of three options: "Mute", "Default", "Follow", unless "Unmute" is currently selected; if so, that option is shown too until you select something different. (You can choose "Unmute" for a topic in a muted channel, and that choice persists when you unmute the channel.)

image image

So that should fully describe which state transitions web supports. Among those, here are the transitions that this zulip-flutter PR doesn't yet support:

In a muted channel:

  • You can't go from "Mute" to "Default" (unless you go through "Follow").
  • You can't go from "Default" to "Mute" (unless you go through "Unmute" or "Follow").
  • You can't go from "Unmute" to "Default" (unless you go through "Follow").

(In a muted channel, the distinction between "Mute" and "Default" matters because it controls whether we show the topic in the whole-stream message list.)

  • You can't go from "Follow" to "Unmute" (unless you go through "Mute" or "Default"). ("Follow" and "Unmute" cause different behavior with notifications.)

In an unmuted channel:

  • You can't go from "Unmute" to "Default" (unless you go through "Mute" or "Follow"). (I'm not sure if "Unmute" and "Default" cause different behavior in an unmuted channel, I think maybe they don't.)

None of those "unless you go through" workarounds are intuitive. Even if you know about them, you might get a surprise when you try going through "Mute" and the conversation disappears from the UI and you can't find it to choose the state you wanted in the first place.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web's radio-buttons approach seems like a fine solution to me, but again we might postpone that; we should make an issue if it's planned though. @gnprice, what do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this PR follows the state transitions in the mobile app very closely, including the UX. If we are going with a newer radio-button design, it will probably save some time to get this right with a Figma redesign.

See discussion here.

Copy link
Collaborator

@chrisbobbe chrisbobbe Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, that discussion is helpful context! I missed it in my review because I didn't see it linked from this PR or the issue. 🙂 I agree with the outcome there:

Yeah, some version of [web's radio-buttons design] would probably be good to put in mobile's action sheet too.

Definitely post-launch, though.

Would you add the link to that discussion on #348, and file an issue for that post-launch task? It can link to my comment here to memoize the work of figuring out what state transitions we're missing. (I used a lot of post-it notes, haha.)

Copy link
Member Author

@PIG208 PIG208 Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Opened #1078 and added the link.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

icons: Take mute/unmute/following icons from the web app

Hmm yeah, looks like the Figma doesn't have icons for these yet. Following web seems fine, but let's note in the commit message that we checked https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=544-22131&node-type=canvas&m=dev and didn't find corresponding icons there.

@@ -59,6 +59,22 @@
"@permissionsDeniedReadExternalStorage": {
"description": "Message for dialog asking the user to grant permissions for external storage read access."
},
"actionSheetOptionMuteTopic": "Mute topic",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action_sheet: Support muting/unmuting/following topics

Bump on #1041 (review) :

Thanks! Comments below, and I agree with your comment at #348 (comment) 🙂

I think a design requirement of this feature is to also display mute/following states for each topic. At this point we can borrow that from the legacy mobile app.

The Fixes: line doesn't belong on this commit until we do that 🙂 or make it not part of #348.

The tests, ApiConnection.{post,patch,delete}, are mostly similar to
each other, because the majority of them are testing that the params
are parsed into the body with the same content type.

If we find the need to update these test cases with new examples, it
will be ripe to refactor them with a helper. Until then, just duplicate
them for simplicity.

Signed-off-by: Zixuan James Li <[email protected]>
For the legacy case, there can be an error when muting a topic that
is already muted or unmuting one that is already unmuted.

The callers are not expected to handle such errors because they aren't
really actionable.

Signed-off-by: Zixuan James Li <[email protected]>
Renamed "mute-new.svg", "unmute-new.svg" to "mute.svg" and "unmute.svg",
respectively.

These are taken from the web app because we checked
https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=544-22131&node-type=canvas&m=dev
and the Figma doesn't have the corresponding icons at the time this is implemented.

See:
  https://github.com/zulip/zulip/tree/da4443f392cc8aa9e6879d905cb1ccd50b66127b/web/shared/icons

Signed-off-by: Zixuan James Li <[email protected]>
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

Signed-off-by: Zixuan James Li <[email protected]>
@PIG208
Copy link
Member Author

PIG208 commented Nov 25, 2024

I have updated the PR to implement MessageListAppBarTitle and to address the review feedback. Thanks @chrisbobbe for the review! The UI change (app bar with two rows) is mostly separate from the topic action sheet sheet, so neither needs to block each other if we decide to implement it as a follow-up.

(sorry about the debug banner on the screenshots!)

Compare to the legacy app
old new
ChannelNarrow 1000015547(1) 1000015545
followed 0ddde80f-fd24-4898-afd6-612853fe2d4b(1) 1000015543
muted / 1000015540
none 1000015551 1000015538
dark / 1000015579
Compare to the legacy app: font sizes
old new
large, channel 1000015567 1000015564
large, topic 1000015569 1000015566
small, channel 1000015575 1000015573
small, topic 1000015577 1000015571
When the channel is unknown
before 1000015556
after 1000015554

@PIG208 PIG208 force-pushed the pr-muting branch 3 times, most recently from 25f5f64 to d38bc57 Compare November 25, 2024 21:40
This mostly follows the legacy mobile app. Notably, this changes the
title text to use normal font weight (wght=400), and breaks the text
into two rows for topic narrow.  This will eventually be superseded
by zulip#1039, so we should keep the implementation as simple as possible for
now.

There are some differences from the old design:

The legacy mobile uses different colors for the title text, depending on
the color of the channel, to make the text more visible.  We currently
don't have that, so the text just uses the ambient color.

The original design also displays the 'mute' icon when the channel is
muted:
  https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/streams/StreamIcon.js#L20-L29
In the Flutter app, however, only the privacy level related
icons are displayed (e.g.: web public, invite only).  We continue to
leave out the 'mute' icon in this implementation.  This can change after
we have a concrete redesign plan.

This implementation also shows the corresponding icons for 'muted' and
'unmuted' topics; previously, only the icon for 'follow' was displayed.
And we continue using the existing icons in the Flutter app, without
trying to match with the exact ones in the old design.

References:
  https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/title/TitleStream.js#L113-L141
  https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/styles/navStyles.js#L5-L18

Fixes: zulip#348

Signed-off-by: Zixuan James Li <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintainer review PR ready for review by Zulip maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

UI for topic muting/unmuting/following
3 participants