-
Notifications
You must be signed in to change notification settings - Fork 321
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
Conversation
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. |
27dd265
to
d0f7d15
Compare
Thanks! This is now ready for review, and I've updated the issue description with changes since the draft. |
d0f7d15
to
399cd6a
Compare
Thanks! Would you also post a few screenshots? |
399cd6a
to
e653144
Compare
Sure! Done. |
There was a problem hiding this 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.
lib/widgets/emoji_reaction.dart
Outdated
// to the underlying Scrollable to remove an unwanted node | ||
// in accessibility focus traversal. | ||
scrollDirection: Axis.horizontal, | ||
physics: ClampingScrollPhysics(), |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I think the default is fine actually :)
I was reading a tab-bar implementation (when doing the Semantics work for this PR) and I think it opted out of the "bouncing" behavior. But I'm not sure this is really a "tab bar" in the same sense as that code I was reading.
lib/widgets/emoji_reaction.dart
Outdated
messageId: widget.messageId, | ||
reactionType: reactionType, | ||
emojiCode: emojiCode, | ||
onRequestSelect: (r) => _setSelection(r), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: tearoff
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() { |
There was a problem hiding this comment.
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:
void _reconcile() { | |
void _reconcile() { | |
_setSelection(_findMatchingReaction()); | |
} |
Then _findMatchingReaction can directly return whenever it's found its answer.
lib/widgets/emoji_reaction.dart
Outdated
if (reactionType == null && widget.initialReactionType != null) { | ||
assert(emojiCode == null); | ||
assert(widget.initialEmojiCode != null); | ||
reactionType = widget.initialReactionType!; | ||
emojiCode = widget.initialEmojiCode!; |
There was a problem hiding this comment.
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.
lib/widgets/emoji_reaction.dart
Outdated
return SizedBox( | ||
width: double.infinity, | ||
child: Column( | ||
mainAxisSize: MainAxisSize.min, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be equivalent?
return SizedBox( | |
width: double.infinity, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.stretch, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah no actually, and I think this is how it is in the released revision 🙂—it start-aligns the emoji items instead of center-aligning them.
Looks like I can get the desired behavior with CrossAxisAlignment.center
instead of stretch, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't quite understand the first sentence — can you spell out which version does what? (I.e. in "this is how" and "it start-aligns", what do "this" and "it" refer to?) Is the behavior in the release different from what was in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sorry, yes that was confusing!
In 30.0.262, the items are start-aligned:

Here's code pasted from the 30.0.262 tag:
Widget build(BuildContext context) {
// TODO could pull out this layout/appearance code,
// focusing this widget only on state management
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
ViewReactionsHeader(widget.pageContext,
I want the items to be center-aligned, not start aligned. Column
's default crossAxisAlignment
is CrossAxisAlignment.center
, so I can accomplish that with
return SizedBox(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
It turned out that the SizedBox
isn't necessary, and I can spell out CrossAxisAlignment.center
if that's helpful. So, as I've had in recent revisions of this PR:
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
which center-aligns the items.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very helpful, thanks! OK, so an earlier version of this PR (as seen in v30.0.262) had "stretch" alignment. That makes the children wide, including the header; and then I think the Row in the header, with defaults MainAxisSize.max and MainAxisAlignment.start, ends up putting the items at the start.
The revised version looks good.
lib/widgets/emoji_reaction.dart
Outdated
(x) => x.reactionType == reactionType && x.emojiCode == emojiCode | ||
)?.userIds.toList(); | ||
|
||
// (No filtering of muted or deactivated users.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// (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.
lib/widgets/emoji_reaction.dart
Outdated
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), |
There was a problem hiding this comment.
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
lib/widgets/emoji_reaction.dart
Outdated
itemBuilder: (context, index) => | ||
ViewReactionsUserItem(context, userId: userIds[index])))); |
There was a problem hiding this comment.
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)
lib/widgets/emoji_reaction.dart
Outdated
Navigator.pop(pageContext); | ||
|
||
Navigator.push(pageContext, | ||
ProfilePage.buildRoute(context: pageContext, userId: userId)); |
There was a problem hiding this comment.
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.
lib/widgets/emoji_reaction.dart
Outdated
onTap: _onPressed, | ||
splashFactory: NoSplash.splashFactory, | ||
overlayColor: WidgetStateColor.resolveWith((states) => | ||
states.any((e) => e == WidgetState.pressed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
states.any((e) => e == WidgetState.pressed) | |
states.contains(WidgetState.pressed) |
Looks like it's a Set, so that should be equivalent (and potentially faster)
e653144
to
45ad6ab
Compare
Thanks for the review! Revision pushed. I noticed on reaction chips that the tooltip-on-long-press was sometimes winning out over the show-view-reactions-sheet, so I added a commit to get rid of the tooltip, with a test change for some tests that had been relying on it. I also added a tweak to the scroll-into-view logic for the horizontal list of emojis, as a separate commit on top. |
And I've just updated the screenshots. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Just a few comments.
lib/widgets/emoji_reaction.dart
Outdated
if (MediaQuery.accessibleNavigationOf(context)) { | ||
// Avoid a _voterNames call with potentially many users, | ||
// except in this case where a computation lag would be very small | ||
// compared to the time saved in accessing the information. | ||
result = Semantics( | ||
label: _semanticsLabelWithVoterNames(store, zulipLocalizations), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I agree the computation time might not be a big deal here. But this feels like it's making the UI more complicated to think about — if the user is using TalkBack or VoiceOver, it's presenting different information that isn't present in the UI we regularly use ourselves. I'd prefer to avoid that complication.
Can this instead just present the same information that's there in the main UI? It's true that getting the list of people who reacted is then an extra step or two for people using a screen-reader; but it's an extra step or two for everyone else, too, so that doesn't seem terrible.
Here's what the Flutter doc on the relevant MediaQueryData field says:
/// Whether the user is using an accessibility service like TalkBack or
/// VoiceOver to interact with the application.
///
/// When this setting is true, features such as timeouts should be disabled or
/// have minimum durations increased.
So this conditional seems rather afield from that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Or for now just present the same information that had been in the tooltip, namely the emoji name.)
lib/widgets/emoji_reaction.dart
Outdated
return SizedBox( | ||
width: double.infinity, | ||
child: Column( | ||
mainAxisSize: MainAxisSize.min, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't quite understand the first sentence — can you spell out which version does what? (I.e. in "this is how" and "it start-aligns", what do "this" and "it" refer to?) Is the behavior in the release different from what was in this PR?
lib/widgets/emoji_reaction.dart
Outdated
@override | ||
void initState() { | ||
super.initState(); | ||
if (reactionType == null && widget.initialReactionType != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reactionType == null
condition is always true now that this is in initState, right?
lib/widgets/emoji_reaction.dart
Outdated
/// When auto-scrolling an emoji into view, | ||
/// this is where the scroll position will land | ||
/// (the min- and max- scroll extent lerped to this value). | ||
double _emojiItemPosition(int index, int aggregatedLength) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lerped "at"?
Or some other/further rewording. I'd read "lerped to X" as meaning that X was the second argument of a lerp
method — conceptually the arguments are start, end, and time.
lib/widgets/emoji_reaction.dart
Outdated
final destination = lerpDouble( | ||
scrollPosition.minScrollExtent, | ||
scrollPosition.maxScrollExtent, | ||
position); | ||
if (destination == null) return; // TODO(log) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition is impossible, right? (From the definition of lerpDouble.) So just use !
.
This fixes the two inconsistencies flagged in discussion: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/bottom.20sheet.20.22Cancel.22.2F.22Close.22.20button/near/2216116 > I think it's reasonable to have both labels, but I think we should > choose them differently than now: > > - "Cancel" when the sheet is about doing an action: [etc.] > > - "Close" when the sheet just presents information or nav options: > [etc.]
…s test So we can add another test that uses it.
We'll use this for a semantics label, coming up.
In a later commit in this series, we'll replace the long-press action with showing a "view-reactions" sheet. We won't want something else that's triggered on long-press, like this tooltip, and the tooltip was sometimes winning the gesture-detection in my testing at that later commit. Remove the tooltip, replacing it with a semantics label, which tests that were relying on the tooltip can use instead.
45ad6ab
to
1d12c74
Compare
Thanks! Revision pushed. |
lib/widgets/emoji_reaction.dart
Outdated
} | ||
|
||
Widget result = SizedBox( | ||
height: 400, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh oops, I meant to leave a TODO(design) tune
here (I think I had it in an earlier revision)—will put it back in my next revision
Thanks! All looks good to me. Please go ahead and merge after that comment tweak. |
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.
For motivation, see the comment on _scrollIntoView.
1d12c74
to
98b94bd
Compare
Done, thanks! |
Fixes #740.
Followup:
Notable changes from previous revision:
Screenshots
I've included some screenshots where there are
and in those, I set the scroll position such that you can see the shadow effect.
Scroll-on-select animation: