Skip to content

Distinguish muted users in UI #1429

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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

sm-sayedi
Copy link
Collaborator

Note: No tests are included in this draft revision.

This PR aims to respect the user's settings for muting other users by distinguishing its related UI appearance. From the list of places in UI mentioned in https://zulip.com/help/mute-a-user, the following are included as they're compatible with the current scope of mobile:

  • Combined feed sent by muted users, including the name, profile picture, and message content, are hidden behind a Click here to reveal banner. A revealed message can later be re-hidden.
Message from muted user (Before) Message from muted user (After)
Revealed muted message Revealed muted message (Long-pressed)
  • Muted users have their name displayed as "Muted user" for emoji reactions, polls, and when displaying the recipients of group direct messages.
Emoji reactions | Poll (Before) Emoji reactions | Poll (After)
Group DMs with muted user (Before) Group DMs with muted user (After)
Recent DMs view (Before) Recent DMs view (After)
  • Recent conversations and other features that display avatars will show a generic user symbol in place of a muted user's profile picture.
Custom profile field (Before) Custom profile field (After)
  • Muted users are excluded from the autocomplete for composing a direct message or mentioning a user.

Fixes: #296

@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch from c34b94b to d80442b Compare March 24, 2025 03:49
@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch 3 times, most recently from 86d0e66 to db3216f Compare May 2, 2025 19:01
@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch from db3216f to 17f2f6b Compare May 6, 2025 02:06
@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch 5 times, most recently from 8ab889f to cddcfcc Compare May 22, 2025 17:30
@sm-sayedi
Copy link
Collaborator Author

sm-sayedi commented May 22, 2025

@chrisbobbe This is now ready for a general review, as we discussed on the call this week. PTAL.

I am now starting to write tests.

@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.9975C6.78948 1.9975 4.9975 3.78948 4.9975 6C4.9975 8.21052 6.78948 10.0025 9 10.0025C11.2105 10.0025 13.0025 8.21052 13.0025 6C13.0025 3.78948 11.2105 1.9975 9 1.9975ZM12.6361 10.77C14.0714 9.67415 14.9975 7.94523 14.9975 6C14.9975 2.68767 12.3123 0.00250244 9 0.00250244C5.68767 0.00250244 3.0025 2.68767 3.0025 6C3.0025 7.94523 3.92857 9.67415 5.36389 10.77C4.3572 11.2147 3.43107 11.8445 2.63781 12.6378C0.950451 14.3252 0.00250244 16.6137 0.00250244 19C0.00250244 19.5509 0.449098 19.9975 1 19.9975C1.55091 19.9975 1.9975 19.5509 1.9975 19C1.9975 17.1428 2.73526 15.3617 4.04849 14.0485C5.36171 12.7353 7.14282 11.9975 9 11.9975C10.8572 11.9975 12.6383 12.7353 13.9515 14.0485C15.2647 15.3617 16.0025 17.1428 16.0025 19C16.0025 19.5509 16.4491 19.9975 17 19.9975C17.5509 19.9975 17.9975 19.5509 17.9975 19C17.9975 16.6137 17.0496 14.3252 15.3622 12.6378C14.5689 11.8445 13.6428 11.2147 12.6361 10.77Z" fill="black"/>
</svg>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This icon file is displayed differently when used as ZulipIcon. In the file, the head section is an outlined circle, but in the app, it is a filled circle.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, fill-rule="evenodd" doesn't work when converted to a font. Here's instructions for fixing it:
https://zulip.readthedocs.io/en/latest/subsystems/icons.html#correcting-icons-with-an-evenodd-fill-rule

@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch from cddcfcc to 3df45ad Compare May 23, 2025 09:50
@chrisbobbe
Copy link
Collaborator

Thanks, glad to see this progress on this helpful feature!

Comments on the first four commits:

451ec5a api: Add InitialSnapshot.mutedUsers
0e1259d user: Add UserStore.mutedUsers and helper methods
4b8e1b5 api: Handle muted_users event
bbf9f9c icons: Add "person", "eye", and "eye_off" icons

Let's put the API-binding changes in their own api: commits (one for the initial snapshot, one for the event), then after those, just one user: commit that adds UserStore.mutedUsers / helper methods and also handles MutedUsersEvent. The earlier api: commit that creates MutedUsersEvent will need something like this to satisfy the analyzer:

      case MutedUsersEvent():
        // TODO handle

but that's easy to do. 🙂

Then please send those revised commits as a new, non-draft PR and label it for Greg's review:

  • Add the "integration review" label
  • Set the "assignees" and "reviewers" metadata

Thanks! I'll start reading the later commits now.

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.

And here's a review of the next commit 🙂:

7072228 msglist: Distinguish messages sent by muted users

Comment on lines +1687 to +1709
class PossibleMutedMessage extends InheritedWidget {
const PossibleMutedMessage({
super.key,
required this.changeMuteStatus,
required super.child,
});

final ValueChanged<bool> changeMuteStatus;

@override
bool updateShouldNotify(covariant PossibleMutedMessage oldWidget) => false;

static PossibleMutedMessage of(BuildContext context) {
final value = context.getInheritedWidgetOfExactType<PossibleMutedMessage>();
assert(value != null, 'No PossibleMutedMessage ancestor');
return value!;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting; the ValueChanged typedef was new to me!

I think a different approach would be worthwhile, though, toward more familiar/transparent state management and to minimize added code in _MessageListPageState.build.

  • On MessageListPageState, add methods like the following:

    /// The "revealed" state of a message from a muted sender.
    ///
    /// See also: [revealMutedMessage], [unrevealMutedMessage].
    bool isMutedMessageRevealed(int messageId);
    
    /// For a message from a muted sender, reveal the sender and content,
    /// replacing the "Muted sender" placeholder.
    void revealMutedMessage(int messageId);
    
    /// For a message from a muted sender, hide the sender and content again
    /// with the "Muted sender" placeholder.
    void unrevealMutedMessage(int messageId);
  • Implement these in _MessageListPageState, backed by a Set of message IDs. The implementations should be simple and don't need to involve the store or any event handling; just simple add/remove/read.

  • Read the dartdoc on MessageListPage.ancestorOf and the implementation comment under it. Since we'll need to call this from a build method, make a prep commit that creates an InheritedWidget, so that ancestorOf can become something like:

      /// The [MessageListPageState] above this context in the tree.
      /// 
      /// Uses the efficient [BuildContext.dependOnInheritedWidgetOfExactType],
      /// so this may be called in a build method.
      static MessageListPageState ancestorOf(BuildContext context) {
        final state = context
          .dependOnInheritedWidgetOfExactType<_MessageListPageInheritedWidget>()
          .state;
        assert(state != null, 'No _MessageListPageInheritedWidget ancestor');
        return state!;
      }

    Ah—in fact I think it might need to be an InheritedNotifier, with the reveal…/unreveal… methods calling notifyListeners, so that widgets update when isMutedMessageRevealed has a new value?

  • HideMutedMessageButton (perhaps renamed UnrevealMutedMessageButton for consistency) can just do this in its onPressed:

    MessageListPage.ancestorOf(pageContext).unrevealMutedMessage(message.id);
  • The "Reveal" button in the message list can do this:

    MessageListPage.ancestorOf(context).revealMutedMessage(message.id);
  • And the UI state in _MessageWithPossibleSenderState.build can use MessageListPage.ancestorOf(context).isMutedMessageRevealed.

Comment on lines +946 to +949
"revealButtonLabel": "Reveal message for muted sender",
"@revealButtonLabel": {
"description": "Label for the button revealing hidden message from a muted sender in message list."
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels verbose; how about just "Reveal message"? The sender row already says "Muted sender".

Comment on lines +942 to +945
"mutedSender": "Muted sender",
"@mutedSender": {
"description": "Name for a muted user to display in message list."
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about "Muted user"? The fact that it's the sender is already communicated by the position of this text.

@@ -128,6 +128,10 @@
"@actionSheetOptionMarkAsUnread": {
"description": "Label for mark as unread button on action sheet."
},
"actionSheetOptionHideMutedMessage": "Hide muted message again",
Copy link
Collaborator

Choose a reason for hiding this comment

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

msglist: Distinguish messages sent by muted users

Commit-message nit: no Fixes: line until a later commit, when all of the issue's work is done :)

class AvatarPlaceholder extends StatelessWidget {
const AvatarPlaceholder({super.key, this.size});

final double? size;
Copy link
Collaborator

Choose a reason for hiding this comment

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

If all callers pass size, let's make it required

Comment on lines +1697 to +1698
// Scale the icon proportionally to match the Figma design.
size: size != null ? size! * 20 / 32 : null,
Copy link
Collaborator

@chrisbobbe chrisbobbe May 24, 2025

Choose a reason for hiding this comment

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

What's the meaning of the size param, and why isn't it used directly here? When I read this I wonder why callers don't just pass the size they want (e.g. from a Figma frame) and expect it to be applied faithfully :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I think I see: size is really about the dimensions of the square gray box, and the icon is meant to be some fraction of that, right?

Oh, but a nuance: the actual size of the box is controlled by AvatarShape, which callers are supposed to use as a wrapper. So I think the size param calls for a dartdoc explaining why it's needed; something like:

  /// The size of the placeholder box.
  ///
  /// This should match the `size` passed to the wrapping [AvatarShape].
  /// The placeholder's "person" icon will be scaled proportionally to this.

Then here, on the arithmetic, an implementation comment like:

        // Where the avatar placeholder appears in the Figma,
        // this is how the icon is sized proportionally to its box.

Comment on lines +297 to +298
required this.grey250,
required this.grey550,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, these aren't Figma design variables in the usual way—

image

—they're part of a "color palette" (see #831).

Since the Figma doesn't seem to offer a variable, let's first see how these look in dark mode (please post screenshots); if they look reasonable (even if not perfect), I'd just hard-code the colors inline, with a TODO(design): dark-mode variant?. If we need a dark-mode variant, please @-mention Vlad in #mobile.

Comment on lines +1513 to +1546
Widget? revealButton;
if (showAsMuted) {
revealButton = TextButton.icon(
onPressed: () {
changeMuteStatus(false);
},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 6),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
splashFactory: NoSplash.splashFactory,
).copyWith(
backgroundColor: WidgetStateColor.fromMap({
WidgetState.pressed: designVariables.neutralButtonBg,
~WidgetState.pressed: designVariables.neutralButtonBg
.withFadedAlpha(0),
}),
foregroundColor: WidgetStateColor.fromMap({
WidgetState.pressed: designVariables.neutralButtonLabel,
~WidgetState.pressed: designVariables.neutralButtonLabel
.withFadedAlpha(0.85),
}),
),
icon: Icon(ZulipIcons.eye),
label: Text(zulipLocalizations.revealButtonLabel,
style: TextStyle(fontSize: 16, height: 1)
.merge(weightVariableTextStyle(context, wght: 600))));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

With 20+ lines of code, this would be good to pull out into a separate widget or a helper method

sm-sayedi added 12 commits May 24, 2025 07:32
The conversations are excluded where all the recipients are muted.
Direct messages of those conversations are excluded where all the
recipients are muted.

Currently the applicable narrows are:
  - CombinedFeedNarrow
  - MentionsNarrow
  - StarredMessagesNarrow

In the future, this will apply to the ReactionsNarrow from the Web app
too, once we have it.
@sm-sayedi sm-sayedi mentioned this pull request May 24, 2025
@sm-sayedi sm-sayedi force-pushed the 296-respect-muted-users branch from 3df45ad to c1a1982 Compare May 24, 2025 03:28
@alya
Copy link
Collaborator

alya commented May 29, 2025

"Reveal message for muted sender" is an odd phrase. I think @terpimost 's design just had "Reveal":

image-1748438975111

Was this changed because we decided to put the button on a separate line from the sender? Even so, maybe "Reveal message" is best -- I think it'll be clear enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mute muted users
4 participants