-
Notifications
You must be signed in to change notification settings - Fork 319
Show user status in UI #1702
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
base: main
Are you sure you want to change the base?
Show user status in UI #1702
Conversation
Copying the review status from #1629 (per #1629 (review)): I've read the first commit except for its tests: and the comments I had there have been resolved. Remaining to review are those tests, and the other 4 commits: |
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 again for building this! Comments below.
I've read through the whole of the first commit:
463b938 msglist: Show user status emoji
and the non-test changes in the other commits:
14093b2 recent-dms: Show user status emoji in recent DMs page
646f9d9 new-dm: Show user status emoji
846261a autocomplete: Show user status emoji in user-mention autocomplete
e9f682e profile: Show user status
For this round, I skipped the tests in the later commits because I think a number of my comments on the first commit's tests will apply to those too. So please go ahead and revise the later tests too where applicable.
test/widgets/message_list_test.dart
Outdated
/// Finder for [UserStatusEmoji] widget. | ||
/// | ||
/// Use [type] to specify the exact emoji child widget. It can be either | ||
/// [UnicodeEmojiWidget] or [ImageEmojiWidget]. | ||
Finder findStatusEmoji(Type type) { | ||
assert(type == UnicodeEmojiWidget || type == ImageEmojiWidget); | ||
return find.ancestor( | ||
of: find.byType(type), | ||
matching: find.byType(UserStatusEmoji)); |
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 seems like it's clearer if just inlined at its call sites.
The implementation (minus the assert, which becomes unnecessary) isn't much longer than the call; and it's more transparent about what it's doing. I also don't feel like this is particularly encapsulating any knowledge of the details of how the UserStatusEmoji widget works.
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.
Also, after doing that: how about just find.byType(UserStatusEmoji)
? It's not clear to me what's gained by adding the condition that the widget have a UnicodeEmojiWidget descendant, or an ImageEmojiWidget descendant.
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.
Also, after doing that: how about just
find.byType(UserStatusEmoji)
?
Copying #1629 (comment) here: 🙂
As we discussed in this week’s check-in call,
UserStatusEmoji
for many cases can contain onlySizedBox.shrink()
, including when there’s no status set for a user. So only usingfind.byType(UserStatusEmoji)
will pass the tests even when we don’t pass status for a user.
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.
(continued in #1702 (comment) below)
test/widgets/message_list_test.dart
Outdated
void checkUserStatusEmoji(Finder emojiFinder, {required bool isAnimated}) { | ||
check((emojiFinder | ||
.evaluate().first.widget as UserStatusEmoji).neverAnimate).equals(!isAnimated); |
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 also feels like it'd be clearer inlined. The actual check is all about neverAnimate
, but the name doesn't sound like that — it sounds like it's going to be doing some other, more general, check.
test/widgets/message_list_test.dart
Outdated
check((emojiFinder | ||
.evaluate().first.widget as UserStatusEmoji).neverAnimate).equals(!isAnimated); |
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.
Instead of finder.evaluate()
, use tester
explicitly:
check((emojiFinder | |
.evaluate().first.widget as UserStatusEmoji).neverAnimate).equals(!isAnimated); | |
check(tester.firstWidget<UserStatusEmoji>(emojiFinder).neverAnimate) | |
.equals(!isAnimated); |
That way it's conceptually clearer what's going on. (The implementation of finder.evaluate
will end up using the same WidgetTester instance anyway, getting hold of it behind the scenes via a singleton.)
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 main motivation for not using tester
was to minimize the number of params passed to the helper methods (checkStatusEmoji
and others in the next commits). Now that it helps in making the tests clearer, so going to use that in the new revision.🙂
test/widgets/message_list_test.dart
Outdated
@@ -91,6 +110,7 @@ void main() { | |||
if (mutedUserIds != null) { | |||
await store.setMutedUsers(mutedUserIds); | |||
} | |||
await store.changeUserStatuses(userStatuses ?? []); |
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.
How about having the individual test cases do this? That way we avoid adding yet another feature to this shared helper. (It probably should have fewer features already — that'd help make fewer things for the reader of each test case to potentially have to think about.)
test/widgets/message_list_test.dart
Outdated
userStatuses: [ | ||
( | ||
user1.userId, | ||
UserStatusChange( | ||
text: OptionSome('Busy'), |
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, these tuples are a bit of a pain to read.
How about making changeUserStatuses
instead take a Map? That seems semantically appropriate, as there's no reason to have the same user ID more than once in the same call. Then:
userStatuses: [ | |
( | |
user1.userId, | |
UserStatusChange( | |
text: OptionSome('Busy'), | |
userStatuses: { | |
user1.userId: UserStatusChange( | |
text: OptionSome('Busy'), |
lib/widgets/autocomplete.dart
Outdated
Flexible(child: labelWidget), | ||
if (option case UserMentionAutocompleteResult(:var userId)) | ||
UserStatusEmoji(userId: userId, size: 18, | ||
padding: const EdgeInsetsDirectional.only(start: 5.0))]), |
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: similarly:
padding: const EdgeInsetsDirectional.only(start: 5.0))]), | |
padding: const EdgeInsetsDirectional.only(start: 5.0)), | |
]), |
lib/widgets/autocomplete.dart
Outdated
children: [ | ||
labelWidget, | ||
Row(children: [ | ||
Flexible(child: labelWidget), | ||
if (option case UserMentionAutocompleteResult(:var userId)) | ||
UserStatusEmoji(userId: userId, size: 18, | ||
padding: const EdgeInsetsDirectional.only(start: 5.0))]), | ||
if (sublabelWidget != null) sublabelWidget, | ||
])), |
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 this new logic is best included as part of labelWidget
. Conceptually it's closely tied to the user's name, which is part of the label.
Probably in fact the cleanest home for this is in the switch (option)
above, which is already the place where we inspect the details of option
.
lib/widgets/profile.dart
Outdated
@@ -73,17 +75,28 @@ class ProfilePage extends StatelessWidget { | |||
), | |||
// TODO write a test where the user is muted; check this and avatar | |||
TextSpan(text: store.userDisplayName(userId, replaceIfMuted: false)), | |||
UserStatusEmoji.asWidgetSpan( | |||
userId: userId, | |||
fontSize: 20, |
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: this says fontSize: 20
but the presence circle above says fontSize: nameStyle.fontSize!
; should say the same thing both places, since the intent is for them to be the same
lib/widgets/profile.dart
Outdated
@@ -47,6 +48,7 @@ class ProfilePage extends StatelessWidget { | |||
if (user == null) { | |||
return const _ProfileErrorPage(); | |||
} | |||
final userStatus = store.getUserStatus(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.
nit: put this next to displayEmail
; conceptually they're here for the very same reason
lib/widgets/profile.dart
Outdated
color: DesignVariables.of(context).userStatusText)), | ||
|
||
if (displayEmail != 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.
Is this blank line intended?
The items just above here feel to me closely related to the items just below, and in particular just as closely related as other items that are grouped next to each other.
eccaa7a
to
686130f
Compare
Thanks @gnprice for the detailed review. Revision pushed, PTAL. Note: CI is failing with the following issue:
I tried pushing several times, but it didn't solve the problem. |
Yeah, that CI failure is unrelated. Filed as #1710, and I'll fix it. |
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 handful of comments this time. I've read the same portions of the branch as last round, plus the two small new commits.
test/widgets/message_list_test.dart
Outdated
await store.changeUserStatuses({ | ||
user.userId: UserStatusChange( | ||
text: OptionSome('Busy'), |
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: can be a bit simpler and more compact by calling changeUserStatus
directly:
await store.changeUserStatuses({ | |
user.userId: UserStatusChange( | |
text: OptionSome('Busy'), | |
await store.changeUserStatus(user.userId, UserStatusChange( | |
text: OptionSome('Busy'), |
Also nice is that that corresponds more directly to what happens in the real server API: each user's status comes in a separate UserStatusEvent.
test/widgets/message_list_test.dart
Outdated
await setupMessageListPage(tester, | ||
users: [user], | ||
messages: [eg.streamMessage(sender: user)], | ||
); |
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: can compact a bit vertically:
await setupMessageListPage(tester, | |
users: [user], | |
messages: [eg.streamMessage(sender: user)], | |
); | |
await setupMessageListPage(tester, | |
users: [user], messages: [eg.streamMessage(sender: user)]); |
Making each test case more compact is helpful for the reader scanning through a given test case to fully understand what it's doing, and especially for scanning through several neighboring test cases to compare them and to think about what situations are covered and what situations there might be that aren't yet.
test/widgets/message_list_test.dart
Outdated
}); | ||
await tester.pump(); | ||
|
||
checkStatusEmoji(tester, type: ImageEmojiWidget, isPresent: true); |
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's kind of puzzling to me that this isn't giving an error, actually: if there's an image emoji here, that should be a RealmContentNetworkImage, which should mean the widget tries to load an image from the network. And I'd expect that to throw an error, given that this test case hasn't set debugNetworkImageHttpClientProvider (e.g. by calling prepareBoringImageHttpClient).
Would you try to get to the bottom of that? I wonder if perhaps this widget tree isn't getting fully built in one frame, and another tester.pump
is needed in order to fully exercise the code.
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 first problem is that it needs another tester.pump
, but even with that, it will not throw an error because we have ImageEmojiWidget.errorBuilder
set. Nevertheless, added prepareBoringImageHttpClient
.
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 see.
How did you determine that it needs another tester.pump
— what's missing after the first pump?
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.
Examining the code again, if ImageEmojiWidget.errorBuilder
is not provided, there is no need for the second pump. Only with the first pump will the code throw the error.
The last time that I concluded that it needs two pumps is because I used a print
statement inside ImageEmojiWidget.errorBuilder
with the assumption that if there is an error, then errorBuilder
is called. But it is triggered after a setState
, which requires the second pump in the test code to see if it is called.
test/widgets/message_list_test.dart
Outdated
}); | ||
await tester.pump(); | ||
|
||
checkStatusEmoji(tester, type: UnicodeEmojiWidget, isPresent: true); |
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.
Instead of find.byType(UnicodeEmojiWidget)
, how about something like find.text("\u{1f6e0}")
? (Continuing from #1702 (comment) .)
That's more nicely end-to-end — it expresses what we're looking for in terms of what the user sees, rather than the way we've organized our code. In this case it makes it a bit more specific; it also means that if we refactor how those widgets work, these tests wouldn't need to change. (Except insofar as we changed how the UI actually looks to the user in ways this is checking for, which is a good reason to need to change it.)
Then I think that opens up a further refactor that would make these a bit more transparent:
checkStatusEmoji(tester, type: UnicodeEmojiWidget, isPresent: true); | |
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); |
The isPresent: false
call sites could become like:
check(find.text('\u{1f6e0}')).findsNothing();
…. But actually, looking at them, the test data in those test cases doesn't supply any emoji in the first place. I think it's fine to not have any such check in that case, then — instead we can rely on an assumption that our code isn't going to invent out of whole cloth an emoji to display as the user's status emoji.
test/widgets/message_list_test.dart
Outdated
final senderRowFinder = find.ancestor(of: nameFinder, | ||
matching: find.ancestor(of: statusEmojiFinder, | ||
matching: find.byType(Row))); | ||
isPresent | ||
? check(senderRowFinder).findsAny() |
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.
Sure, that works.
It does introduce some degree of dependence on the internals of how we've organized our widgets (the existence of a SenderRow widget class, and the fact that it should contain the status emoji). But I think the concept of a "sender row" is a pretty stable aspect of our UI, so that the only way that assumption is likely to break is if we're changing the real user-visible UI in a way that alters the user-facing fact that the sender's status emoji belongs on a row together with their name etc.
title = TextSpan(children: [ | ||
TextSpan(text: store.selfUser.fullName), |
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: can simplify these a bit by using both text
and children
:
title = TextSpan(children: [ | |
TextSpan(text: store.selfUser.fullName), | |
title = TextSpan(text: store.selfUser.fullName, children: [ |
(See the docs for those two TextSpan fields.)
Looking at the TextSpan implementation, I think that saves a small amount of work by eliminating that indirection. There's potentially a lot of these on the screen, so that savings is potentially useful too.
lib/widgets/autocomplete.dart
Outdated
label = store.userDisplayName(userId); | ||
emoji = UserStatusEmoji(userId: userId, size: 18, | ||
padding: const EdgeInsetsDirectional.only(start: 5.0)); | ||
sublabel = store.userDisplayEmail(userId); | ||
case WildcardMentionAutocompleteResult(:var wildcardOption): | ||
avatar = SizedBox.square(dimension: 36, | ||
child: const Icon(ZulipIcons.three_person, size: 24)); | ||
emoji = null; | ||
label = wildcardOption.canonicalString; |
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: order label vs. emoji consistently
(probably label then emoji, to correspond to how they're displayed)
b14ee65
to
cd4a1a6
Compare
Thanks for the review. New revision pushed. |
emojiCode: '1f6e0', reactionType: ReactionType.unicodeEmoji)))); | ||
await tester.pump(); | ||
|
||
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); |
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.
For this commit and the following ones, should we make find.text('\u{1f6e0}')
an implementation detail of checkFindsStatusEmoji
as it stays the same?
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! Comments below on the changes.
lib/widgets/new_dm_sheet.dart
Outdated
TextSpan(children: [ | ||
TextSpan(text: store.userDisplayName(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.
same as #1702 (comment)
test/widgets/message_list_test.dart
Outdated
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); | ||
check(find.descendant(of: find.byType(SenderRow), | ||
matching: find.text('Busy'))).findsNothing(); |
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 test is a bit stronger if the "finds nothing" check isn't limited by SenderRow:
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); | |
check(find.descendant(of: find.byType(SenderRow), | |
matching: find.text('Busy'))).findsNothing(); | |
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); | |
check(find.text('Busy')).findsNothing(); |
With the SenderRow restriction, the reader has to wonder if perhaps the status text is getting shown after all, and just doesn't happen to be within a widget called SenderRow. (Perhaps something changed in the organization of the UI code.)
This is a bit different from the situation at #1702 (comment) (where introducing SenderRow was OK), because the check has the opposite polarity. In that test, adding a SenderRow condition makes the check more demanding — so if the code gets reorganized in a way that breaks the test's implicit assumptions about how it's organized, then the test will fail and we'll know we need to update it. But here, adding SenderRow makes it less demanding — so in that situation the check wouldn't fail, but instead would become vacuous, less able to fail when it should.
test/widgets/message_list_test.dart
Outdated
text: OptionSome('Coding'), | ||
emoji: OptionSome(StatusEmoji(emojiName: 'zulip', | ||
emojiCode: 'zulip', reactionType: ReactionType.zulipExtraEmoji)))); | ||
await tester.pumpAndSettle(); |
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.
Best to avoid pumpAndSettle: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-using-pumpandsettle
Can this be two pumps? Better to be explicit that way.
test/widgets/message_list_test.dart
Outdated
}); | ||
await tester.pump(); | ||
|
||
checkStatusEmoji(tester, type: ImageEmojiWidget, isPresent: true); |
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 see.
How did you determine that it needs another tester.pump
— what's missing after the first pump?
test/widgets/message_list_test.dart
Outdated
final senderRowFinder = find.ancestor(of: statusEmojiFinder, | ||
matching: find.byType(SenderRow)); | ||
check(senderRowFinder).findsOne(); |
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: I think this would be just as clear if inlined, and a bit shorter. It's saying the status emoji has a SenderRow as an ancestor.
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.
Then also can drop the blank line above, and join this function's whole body in one stanza.
test/widgets/message_list_test.dart
Outdated
check(statusEmojiFinder).findsOne(); | ||
check(tester.firstWidget<UserStatusEmoji>(statusEmojiFinder) |
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: since we're expecting this to find just one widget anyway, can simplify firstWidget
to widget
:
check(statusEmojiFinder).findsOne(); | |
check(tester.firstWidget<UserStatusEmoji>(statusEmojiFinder) | |
check(statusEmojiFinder).findsOne(); | |
check(tester.widget<UserStatusEmoji>(statusEmojiFinder) |
Status emojis are only shown for self-1:1 and 1:1 conversation items. They're ignored for group conversations as that's what the Web does.
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.
Thank @gnprice for the review. New changes pushed.
await tester.pump(); | ||
|
||
checkFindsStatusEmoji(tester, find.text('\u{1f6e0}')); | ||
check(find.textContaining('Busy')).findsNothing(); |
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.
In places where status emoji is used as a span, I used find.textContaining
instead of find.text
, so if status text is shown in a text span, these tests can detect it.
checkFindsTileStatusEmoji(tester, user, find.text('\u{1f6e0}')); | ||
check(find.descendant(of: findUserTile(user), | ||
matching: find.textContaining('Busy'))).findsNothing(); | ||
check(findUserChip(user)).findsOne(); | ||
checkFindsChipStatusEmoji(tester, user, find.text('\u{1f6e0}')); | ||
check(find.descendant(of: findUserChip(user), | ||
matching: find.text('Busy'))).findsNothing(); |
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 still kept this check for the status texts the same as before (didn't remove find.descendant
), as there could be status texts in two places on the same page (one for the user tile, another for the user chip).
The remaining half of #1629. For related images, please see #1629 description.
Fixes: #197