Skip to content

msglist: Support viewing who reacted to a message #1700

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

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

Conversation

chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe commented Jul 10, 2025

Fixes #740.

Followup:

Notable changes from previous revision:

  • "See who reacted" button omitted in action sheet when no reactions
  • Emojis centered horizontally when few
  • Emojis scroll into view when tapped
  • Reasonable UI with VoiceOver (at least in my testing)
  • Wrote some basic tests

Screenshots

I've included some screenshots where there are

  • many emoji
  • many users

and in those, I set the scroll position such that you can see the shadow effect.

Light Dark
image image
image image
image image
image image

Scroll-on-select animation:

Jul-25-2025 15-10-03

@chrisbobbe chrisbobbe requested a review from gnprice July 10, 2025 00:50
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label Jul 10, 2025
@gnprice
Copy link
Member

gnprice commented Jul 10, 2025

Thanks! I skimmed through this and I'm pretty confident it won't break any existing functionality. I also saw you demonstrate this running on your device, and it seemed to work well. So I plan to include this in today's upcoming release, without yet merging to main.

@sm-sayedi sm-sayedi mentioned this pull request Jul 15, 2025
@chrisbobbe chrisbobbe force-pushed the pr-see-who-reacted branch from 27dd265 to d0f7d15 Compare July 25, 2025 21:44
@chrisbobbe chrisbobbe marked this pull request as ready for review July 25, 2025 21:46
@chrisbobbe
Copy link
Collaborator Author

Thanks! This is now ready for review, and I've updated the issue description with changes since the draft.

@chrisbobbe chrisbobbe force-pushed the pr-see-who-reacted branch from d0f7d15 to 399cd6a Compare July 25, 2025 21:47
@gnprice
Copy link
Member

gnprice commented Jul 25, 2025

Thanks! Would you also post a few screenshots?

I experimented with using Semantics to help write human-centered
tests, and I ended up adding some configuration that actually seemed
to make a reasonable experience in the UI, at least in my testing
with VoiceOver.

Fixes zulip#740.
@chrisbobbe chrisbobbe force-pushed the pr-see-who-reacted branch from 399cd6a to e653144 Compare July 25, 2025 22:04
@chrisbobbe
Copy link
Collaborator Author

Sure! Done.

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!

Generally this looks good. Here's a full review except the tests.

// to the underlying Scrollable to remove an unwanted node
// in accessibility focus traversal.
scrollDirection: Axis.horizontal,
physics: ClampingScrollPhysics(),
Copy link
Member

Choose a reason for hiding this comment

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

Interesting — why this instead of the default?

(I believe the default would be this on Android, but BouncingScrollPhysics on iOS.)

messageId: widget.messageId,
reactionType: reactionType,
emojiCode: emojiCode,
onRequestSelect: (r) => _setSelection(r),
Copy link
Member

Choose a reason for hiding this comment

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

nit: tearoff

Suggested change
onRequestSelect: (r) => _setSelection(r),
onRequestSelect: _setSelection,


/// Check that the given reaction still has votes;
/// if not, select a different one if possible or clear the selection.
void _reconcile() {
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this always concludes with _setSelection. It might be a little clearer to read by making that explicit up front:

Suggested change
void _reconcile() {
void _reconcile() {
_setSelection(_findMatchingReaction());
}

Then _findMatchingReaction can directly return whenever it's found its answer.

Comment on lines +722 to +726
if (reactionType == null && widget.initialReactionType != null) {
assert(emojiCode == null);
assert(widget.initialEmojiCode != null);
reactionType = widget.initialReactionType!;
emojiCode = widget.initialEmojiCode!;
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't depend on the store, does it? It looks like it doesn't use the context at all; so it could run as early as initState.

And conversely it doesn't sound like it'd be desirable for this to run repeatedly, or after a new store — these are the initial emoji type and code, after all.

Comment on lines +741 to +744
return SizedBox(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
Copy link
Member

Choose a reason for hiding this comment

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

Would this be equivalent?

Suggested change
return SizedBox(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,

(x) => x.reactionType == reactionType && x.emojiCode == emojiCode
)?.userIds.toList();

// (No filtering of muted or deactivated users.)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// (No filtering of muted or deactivated users.)
// (No filtering of muted or deactivated users.
// Muted users will be shown as muted.)

My first reaction to this comment was "is that right? seems like we shouldn't show muted users here." Then I remembered that that's probably handled at a different layer, and indeed it looks like it is.

Comment on lines +967 to +973
Widget result = InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: SizedBox(
height: 400, // TODO(design) tune
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8),
Copy link
Member

Choose a reason for hiding this comment

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

nit: move SizedBox outward, so the InsetShadowBox is right next to (in the source code) the padding that needs to match it

Comment on lines +975 to +976
itemBuilder: (context, index) =>
ViewReactionsUserItem(context, userId: userIds[index]))));
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as I told Sayed last week 🙂 : #1706 (comment)

(on code that I guess was modeled in part on this PR, so no coincidence)

Comment on lines +999 to +1002
Navigator.pop(pageContext);

Navigator.push(pageContext,
ProfilePage.buildRoute(context: pageContext, userId: userId));
Copy link
Member

Choose a reason for hiding this comment

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

I think the reason this works even though this pageContext isn't really a page context is that there's no await here.

The reason our action-sheet buttons often need a real page context is that they'll dismiss the action sheet, then do something that takes time like a network request, and then after that completes they need to use a context to do something with the result, or to show an error. So if the request takes longer than the few hundred ms of the dismiss animation, the action sheet's own context may be unmounted by that point.

For just synchronously acting on the navigation like this, the widget's own context is fine.

onTap: _onPressed,
splashFactory: NoSplash.splashFactory,
overlayColor: WidgetStateColor.resolveWith((states) =>
states.any((e) => e == WidgetState.pressed)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
states.any((e) => e == WidgetState.pressed)
states.contains(WidgetState.pressed)

Looks like it's a Set, so that should be equivalent (and potentially faster)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

See who left an emoji reaction
2 participants