-
Notifications
You must be signed in to change notification settings - Fork 309
ui: Distinguish bot users in more places #764
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import '../api/model/model.dart'; | |
import '../model/content.dart'; | ||
import '../model/narrow.dart'; | ||
import 'content.dart'; | ||
import 'icons.dart'; | ||
import 'message_list.dart'; | ||
import 'page.dart'; | ||
import 'store.dart'; | ||
|
@@ -46,10 +47,23 @@ class ProfilePage extends StatelessWidget { | |
Center( | ||
child: Avatar(userId: userId, size: 200, borderRadius: 200 / 8)), | ||
const SizedBox(height: 16), | ||
Text(user.fullName, | ||
textAlign: TextAlign.center, | ||
style: _TextStyles.primaryFieldText | ||
.merge(weightVariableTextStyle(context, wght: 700))), | ||
Row( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: [ | ||
if (user.isBot) ...[ | ||
const Icon( | ||
ZulipIcons.bot, | ||
size: 22, | ||
color: Color.fromARGB(255, 159, 173, 173), | ||
), | ||
const SizedBox(width: 10), | ||
], | ||
Flexible(child: Text(user.fullName, | ||
textAlign: TextAlign.center, | ||
style: _TextStyles.primaryFieldText | ||
.merge(weightVariableTextStyle(context, wght: 700)))), | ||
], | ||
), | ||
Comment on lines
-49
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would look better to style this like we do in the other places: first the name, then the bot icon, as an inline |
||
// TODO(#291) render email field | ||
Text(roleToLabel(user.role, zulipLocalizations), | ||
textAlign: TextAlign.center, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,24 +91,29 @@ class RecentDmConversationsItem extends StatelessWidget { | |
final store = PerAccountStoreWidget.of(context); | ||
final selfUser = store.users[store.selfUserId]!; | ||
|
||
final String title; | ||
final List<({String name, bool isBot})> users; | ||
final Widget avatar; | ||
switch (narrow.otherRecipientIds) { // TODO dedupe with DM items in [InboxPage] | ||
case []: | ||
title = selfUser.fullName; | ||
users = [(name: selfUser.fullName, isBot: selfUser.isBot)]; | ||
avatar = AvatarImage(userId: selfUser.userId, size: _avatarSize); | ||
case [var otherUserId]: | ||
// TODO(#296) actually don't show this row if the user is muted? | ||
// (should we offer a "spam folder" style summary screen of recent | ||
// 1:1 DM conversations from muted users?) | ||
final otherUser = store.users[otherUserId]; | ||
title = otherUser?.fullName ?? '(unknown user)'; | ||
users = [(name: otherUser?.fullName ?? '(unknown user)', | ||
isBot: otherUser?.isBot ?? false)]; | ||
avatar = AvatarImage(userId: otherUserId, size: _avatarSize); | ||
default: | ||
// TODO(i18n): List formatting, like you can do in JavaScript: | ||
// new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya']) | ||
// // 'Chris、Greg、Alya' | ||
title = narrow.otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)').join(', '); | ||
users = narrow.otherRecipientIds.map((id) { | ||
final user = store.users[id]; | ||
return (name: user?.fullName ?? '(unknown user)', | ||
isBot: user?.isBot ?? false); | ||
}).toList(); | ||
// TODO(#95) need dark-theme color | ||
avatar = ColoredBox(color: const Color(0x33808080), | ||
child: Center( | ||
|
@@ -131,7 +136,7 @@ class RecentDmConversationsItem extends StatelessWidget { | |
const SizedBox(width: 8), | ||
Expanded(child: Padding( | ||
padding: const EdgeInsets.symmetric(vertical: 4), | ||
child: Text( | ||
child: Text.rich( | ||
style: const TextStyle( | ||
fontSize: 17, | ||
height: (20 / 17), | ||
|
@@ -140,7 +145,19 @@ class RecentDmConversationsItem extends StatelessWidget { | |
), | ||
maxLines: 2, | ||
overflow: TextOverflow.ellipsis, | ||
title))), | ||
TextSpan(children: List.generate(users.length, (index) { | ||
final user = users[index]; | ||
return TextSpan(text: user.name, children: [ | ||
if (user.isBot) ...[ | ||
const WidgetSpan(child: SizedBox(width: 5)), | ||
const WidgetSpan( | ||
child: Icon(ZulipIcons.bot, size: 15, | ||
color: Color.fromARGB(255, 159, 173, 173)), | ||
alignment: PlaceholderAlignment.middle), | ||
], | ||
if (index < users.length - 1) const TextSpan(text: ', '), | ||
]); | ||
}).toList())))), | ||
Comment on lines
-143
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see a comment on the // TODO dedupe with DM items in [InboxPage] and that reminds me that we also show users' names in the Inbox page, when there are unread DMs. Would you add a prep commit that makes a helper function that can be used in this page and the Inbox page too? Perhaps it can return a |
||
const SizedBox(width: 12), | ||
unreadCount > 0 | ||
? Padding(padding: const EdgeInsetsDirectional.only(end: 16), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -139,7 +139,8 @@ void main() { | |
// TODO(#232): syntax like `check(find(…), findsOneWidget)` | ||
final widget = tester.widget(find.descendant( | ||
of: find.byType(RecentDmConversationsItem), | ||
matching: find.text(expectedText), | ||
matching: find.byWidgetPredicate((widget) => widget is Text | ||
&& (widget.textSpan?.toPlainText(includePlaceholders: false) ?? '') == expectedText), | ||
Comment on lines
-142
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm getting some test failures at this commit, but they go away in the next commit. I think it's fine to squash the test change in with the change that fixes the failures. It's not uncommon (though can be helpful to avoid when possible) that a test ends up depending on some implementation detail, and needs to be updated when that detail changes. |
||
)); | ||
if (expectedLines != null) { | ||
final renderObject = tester.renderObject<RenderParagraph>(find.byWidget(widget)); | ||
|
@@ -149,6 +150,14 @@ void main() { | |
} | ||
} | ||
|
||
void checkBotIcon({required int count}) { | ||
final botFinder = find.descendant( | ||
of: find.byType(RecentDmConversationsItem), | ||
matching: find.byIcon(ZulipIcons.bot)).hitTestable(); | ||
|
||
check(botFinder.evaluate().length).equals(count); | ||
} | ||
|
||
Future<void> markMessageAsRead(WidgetTester tester, Message message) async { | ||
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); | ||
await store.handleEvent(UpdateMessageFlagsAddEvent( | ||
|
@@ -176,6 +185,7 @@ void main() { | |
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, eg.selfUser.fullName); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('short name takes one line', (WidgetTester tester) async { | ||
|
@@ -184,6 +194,7 @@ void main() { | |
await setupPage(tester, users: [], dmMessages: [message], | ||
newNameForSelfUser: name); | ||
checkTitle(tester, name, 1); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('very long name takes two lines (must be ellipsized)', (WidgetTester tester) async { | ||
|
@@ -192,6 +203,7 @@ void main() { | |
await setupPage(tester, users: [], dmMessages: [message], | ||
newNameForSelfUser: name); | ||
checkTitle(tester, name, 2); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('unread counts', (WidgetTester tester) async { | ||
|
@@ -205,39 +217,95 @@ void main() { | |
}); | ||
|
||
group('1:1', () { | ||
testWidgets('has right title/avatar', (WidgetTester tester) async { | ||
final user = eg.user(userId: 1); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
group('has right avatar/title', () { | ||
Future<void> checkRecipient(WidgetTester tester, { | ||
required bool isBot, | ||
required bool looksBot | ||
}) async { | ||
final user = eg.user(userId: 1, isBot: isBot); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, user.fullName); | ||
checkBotIcon(count: looksBot ? 1 : 0); | ||
} | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, user.fullName); | ||
testWidgets('bot recipient -> shows bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: true, looksBot: true); | ||
}); | ||
|
||
testWidgets('non-bot recipient -> shows no bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: false, looksBot: false); | ||
}); | ||
}); | ||
|
||
testWidgets('no error when user somehow missing from store.users', (WidgetTester tester) async { | ||
final user = eg.user(userId: 1); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, | ||
users: [], // exclude user | ||
dmMessages: [message], | ||
); | ||
group('no error when user somehow missing from store.users', () { | ||
Future<void> checkRecipient(WidgetTester tester, { | ||
required bool isBot, | ||
required bool looksBot | ||
}) async { | ||
final user = eg.user(userId: 1, isBot: isBot); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, | ||
users: [], // exclude user | ||
dmMessages: [message], | ||
); | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, '(unknown user)'); | ||
checkBotIcon(count: looksBot ? 1 : 0); | ||
} | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, '(unknown user)'); | ||
testWidgets('bot recipient -> shows no bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: true, looksBot: false); | ||
}); | ||
|
||
testWidgets('non-bot recipient -> shows no bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: false, looksBot: false); | ||
}); | ||
Comment on lines
+260
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. group('no error when user somehow missing from store.users', () {
// […]
testWidgets('bot recipient -> shows no bot icon', (tester) async {
await checkRecipient(tester, isBot: true, looksBot: false);
});
testWidgets('non-bot recipient -> shows no bot icon', (tester) async {
await checkRecipient(tester, isBot: false, looksBot: false);
}); The "bot recipient" test doesn't actually simulate a bot recipient, right, because these tests are checking what happens when the user data is missing. I see that the Simplest I think to leave this test is it was, but with an added |
||
}); | ||
|
||
testWidgets('short name takes one line', (WidgetTester tester) async { | ||
final user = eg.user(userId: 1, fullName: 'Short name'); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
checkTitle(tester, user.fullName, 1); | ||
group('short name takes one line', () { | ||
Future<void> checkRecipient(WidgetTester tester, { | ||
required bool isBot, | ||
required bool looksBot | ||
}) async { | ||
final user = eg.user(userId: 1, fullName: 'Short name', isBot: isBot); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
checkTitle(tester, user.fullName, 1); | ||
checkBotIcon(count: looksBot ? 1 : 0); | ||
} | ||
|
||
testWidgets('bot recipient -> shows bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: true, looksBot: true); | ||
}); | ||
|
||
testWidgets('non-bot recipient -> shows no bot icon', (tester) async { | ||
await checkRecipient(tester, isBot: false, looksBot: false); | ||
}); | ||
}); | ||
|
||
testWidgets('very long name takes two lines (must be ellipsized)', (WidgetTester tester) async { | ||
final user = eg.user(userId: 1, fullName: 'Long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name'); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
checkTitle(tester, user.fullName, 2); | ||
group('very long name takes two lines (must be ellipsized)', () { | ||
Future<void> checkRecipient(WidgetTester tester, { | ||
required bool isBot, | ||
required bool looksBot | ||
}) async { | ||
final user = eg.user(userId: 1, isBot: isBot, fullName: 'Long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name'); | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user]); | ||
await setupPage(tester, users: [user], dmMessages: [message]); | ||
checkTitle(tester, user.fullName, 2); | ||
checkBotIcon(count: looksBot ? 1 : 0); | ||
} | ||
|
||
testWidgets('bot recipient -> shows no bot icon', (WidgetTester tester) async { | ||
await checkRecipient(tester, isBot: true, looksBot: false); | ||
}); | ||
Comment on lines
+302
to
+304
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would like the bot icon to appear even when the name is long; see Alya's comment: #764 (comment) If that's hard to do and has to be left as a TODO, that might be OK, but we shouldn't have a test that confirms the undesired behavior. 🙂 |
||
|
||
testWidgets('non-bot recipient -> shows no bot icon', (WidgetTester tester) async { | ||
await checkRecipient(tester, isBot: false, looksBot: false); | ||
}); | ||
}); | ||
|
||
testWidgets('unread counts', (WidgetTester tester) async { | ||
|
@@ -251,51 +319,59 @@ void main() { | |
}); | ||
|
||
group('group', () { | ||
List<User> usersList(int count) { | ||
List<User> usersList(int count, {List<int>? botUsers}) { | ||
assert(() { | ||
if (botUsers == null) return true; | ||
for (int userIndex in botUsers) { | ||
if (userIndex >= count) return false; | ||
} | ||
return true; | ||
}()); | ||
final result = <User>[]; | ||
for (int i = 0; i < count; i++) { | ||
result.add(eg.user(userId: i, fullName: 'User ${i.toString()}')); | ||
result.add(eg.user(userId: i, fullName: 'User ${i.toString()}', | ||
isBot: botUsers?.contains(i) ?? false)); | ||
} | ||
return result; | ||
} | ||
|
||
testWidgets('has right title/avatar', (WidgetTester tester) async { | ||
final users = usersList(2); | ||
final user0 = users[0]; | ||
final user1 = users[1]; | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user0, user1]); | ||
final users = usersList(3, botUsers: [0, 2]); | ||
final message = eg.dmMessage(from: eg.selfUser, to: users); | ||
await setupPage(tester, users: users, dmMessages: [message]); | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, '${user0.fullName}, ${user1.fullName}'); | ||
checkTitle(tester, users.map((u) => u.fullName).join(', ')); | ||
checkBotIcon(count: 2); | ||
}); | ||
|
||
testWidgets('no error when one user somehow missing from store.users', (WidgetTester tester) async { | ||
final users = usersList(2); | ||
final user0 = users[0]; | ||
final user1 = users[1]; | ||
final message = eg.dmMessage(from: eg.selfUser, to: [user0, user1]); | ||
final users = usersList(2, botUsers: [1]); | ||
final message = eg.dmMessage(from: eg.selfUser, to: users); | ||
await setupPage(tester, | ||
users: [user0], // exclude user1 | ||
users: [users[0]], // exclude user[1], which is bot | ||
dmMessages: [message], | ||
); | ||
|
||
checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); | ||
checkTitle(tester, '${user0.fullName}, (unknown user)'); | ||
checkTitle(tester, '${users[0].fullName}, (unknown user)'); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('few names takes one line', (WidgetTester tester) async { | ||
final users = usersList(2); | ||
final message = eg.dmMessage(from: eg.selfUser, to: users); | ||
await setupPage(tester, users: users, dmMessages: [message]); | ||
checkTitle(tester, users.map((u) => u.fullName).join(', '), 1); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('very many names takes two lines (must be ellipsized)', (WidgetTester tester) async { | ||
final users = usersList(40); | ||
final message = eg.dmMessage(from: eg.selfUser, to: users); | ||
await setupPage(tester, users: users, dmMessages: [message]); | ||
checkTitle(tester, users.map((u) => u.fullName).join(', '), 2); | ||
checkBotIcon(count: 0); | ||
}); | ||
|
||
testWidgets('unread counts', (WidgetTester tester) async { | ||
|
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.
Let's put this in the new
DesignVariables
, and use the new variable here and in the message list (where we added the bot icon for senders in #605).