Skip to content

Commit a78d4e0

Browse files
committed
autocomplete: Add user avatars to user-mention autocompletes
Fixes: #227
1 parent dc09d43 commit a78d4e0

File tree

3 files changed

+66
-21
lines changed

3 files changed

+66
-21
lines changed

lib/widgets/autocomplete.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22

3+
import 'content.dart';
34
import 'store.dart';
45
import '../model/autocomplete.dart';
56
import '../model/compose.dart';
@@ -119,19 +120,25 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
119120

120121
Widget _buildItem(BuildContext _, int index) {
121122
final option = _resultsToDisplay[index];
123+
Widget avatar;
122124
String label;
123125
switch (option) {
124126
case UserMentionAutocompleteResult(:var userId):
125-
// TODO(#227) avatar
127+
avatar = Avatar(userId: userId, size: 28, borderRadius: 3);
126128
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
127129
}
128130
return InkWell(
129131
onTap: () {
130132
_onTapOption(option);
131133
},
132134
child: Padding(
133-
padding: const EdgeInsets.all(16.0),
134-
child: Text(label)));
135+
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
136+
child: Row(
137+
children: [
138+
avatar,
139+
const SizedBox(width: 8),
140+
Text(label),
141+
])));
135142
}
136143

137144
@override

test/widgets/autocomplete_test.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import 'package:zulip/api/model/model.dart';
66
import 'package:zulip/api/route/messages.dart';
77
import 'package:zulip/model/compose.dart';
88
import 'package:zulip/model/narrow.dart';
9+
import 'package:zulip/model/store.dart';
910
import 'package:zulip/widgets/message_list.dart';
1011
import 'package:zulip/widgets/store.dart';
1112

1213
import '../api/fake_api.dart';
1314
import '../example_data.dart' as eg;
1415
import '../model/binding.dart';
1516
import '../model/test_store.dart';
17+
import 'content_test.dart';
1618

1719
/// Simulates loading a [MessageListPage] and tapping to focus the compose input.
1820
///
@@ -39,6 +41,8 @@ Future<Finder> setupToComposeInput(WidgetTester tester, {
3941
messages: [message],
4042
).toJson());
4143

44+
prepareBoringImageHttpClient();
45+
4246
await tester.pumpWidget(
4347
MaterialApp(
4448
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
@@ -65,10 +69,18 @@ void main() {
6569
TestZulipBinding.ensureInitialized();
6670

6771
group('ComposeAutocomplete', () {
72+
73+
Finder findNetworkImage(String url) {
74+
return find.byWidgetPredicate((widget) =>
75+
widget is Image &&
76+
widget.image is NetworkImage &&
77+
(widget.image as NetworkImage).url == url);
78+
}
79+
6880
testWidgets('options appear, disappear, and change correctly', (WidgetTester tester) async {
69-
final user1 = eg.user(userId: 1, fullName: 'User One');
70-
final user2 = eg.user(userId: 2, fullName: 'User Two');
71-
final user3 = eg.user(userId: 3, fullName: 'User Three');
81+
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png');
82+
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png');
83+
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png');
7284
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
7385
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
7486

@@ -77,34 +89,59 @@ void main() {
7789
await tester.enterText(composeInputFinder, 'hello @user ');
7890
await tester.enterText(composeInputFinder, 'hello @user t');
7991
await tester.pumpAndSettle(); // async computation; options appear
92+
8093
// "User Two" and "User Three" appear, but not "User One"
8194
check(tester.widgetList(find.text('User One'))).isEmpty();
95+
final user1AvatarFinder =
96+
findNetworkImage(store.tryResolveUrl('user1.png').toString());
97+
check(tester.widgetList(user1AvatarFinder)).isEmpty();
98+
8299
tester.widget(find.text('User Two'));
100+
final user2AvatarFinder =
101+
findNetworkImage(store.tryResolveUrl('user2.png').toString());
102+
tester.widget(user2AvatarFinder);
103+
83104
tester.widget(find.text('User Three'));
105+
final user3AvatarFinder =
106+
findNetworkImage(store.tryResolveUrl('user3.png').toString());
107+
tester.widget(user3AvatarFinder);
84108

85109
// Finishing autocomplete updates compose box; causes options to disappear
86110
await tester.tap(find.text('User Three'));
87111
await tester.pump();
88112
check(tester.widget<TextField>(composeInputFinder).controller!.text)
89113
.contains(mention(user3, users: store.users));
90114
check(tester.widgetList(find.text('User One'))).isEmpty();
115+
check(tester.widgetList(user1AvatarFinder)).isEmpty();
116+
91117
check(tester.widgetList(find.text('User Two'))).isEmpty();
118+
check(tester.widgetList(user2AvatarFinder)).isEmpty();
119+
92120
check(tester.widgetList(find.text('User Three'))).isEmpty();
121+
check(tester.widgetList(user3AvatarFinder)).isEmpty();
93122

94123
// Then a new autocomplete intent brings up options again
95124
// TODO(#226): Remove this extra edit when this bug is fixed.
96125
await tester.enterText(composeInputFinder, 'hello @user tw');
97126
await tester.enterText(composeInputFinder, 'hello @user two');
98127
await tester.pumpAndSettle(); // async computation; options appear
99128
tester.widget(find.text('User Two'));
129+
tester.widget(user2AvatarFinder);
100130

101131
// Removing autocomplete intent causes options to disappear
102132
// TODO(#226): Remove one of these edits when this bug is fixed.
103133
await tester.enterText(composeInputFinder, '');
104134
await tester.enterText(composeInputFinder, ' ');
105135
check(tester.widgetList(find.text('User One'))).isEmpty();
136+
check(tester.widgetList(user1AvatarFinder)).isEmpty();
137+
106138
check(tester.widgetList(find.text('User Two'))).isEmpty();
139+
check(tester.widgetList(user2AvatarFinder)).isEmpty();
140+
107141
check(tester.widgetList(find.text('User Three'))).isEmpty();
142+
check(tester.widgetList(user3AvatarFinder)).isEmpty();
143+
144+
debugNetworkImageHttpClientProvider = null;
108145
});
109146
});
110147
}

test/widgets/content_test.dart

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ import 'dialog_checks.dart';
2727
import 'message_list_checks.dart';
2828
import 'page_checks.dart';
2929

30+
31+
/// Set [debugNetworkImageHttpClientProvider] to return a constant image.
32+
///
33+
/// Returns the [FakeImageHttpClient] that handles the requests.
34+
///
35+
/// The caller must set [debugNetworkImageHttpClientProvider] back to null
36+
/// before the end of the test.
37+
FakeImageHttpClient prepareBoringImageHttpClient() {
38+
final httpClient = FakeImageHttpClient();
39+
debugNetworkImageHttpClientProvider = () => httpClient;
40+
httpClient.request.response
41+
..statusCode = HttpStatus.ok
42+
..content = kSolidBlueAvatar;
43+
return httpClient;
44+
}
45+
3046
void main() {
3147
// For testing a new content feature:
3248
//
@@ -64,21 +80,6 @@ void main() {
6480
});
6581
}
6682

67-
/// Set [debugNetworkImageHttpClientProvider] to return a constant image.
68-
///
69-
/// Returns the [FakeImageHttpClient] that handles the requests.
70-
///
71-
/// The caller must set [debugNetworkImageHttpClientProvider] back to null
72-
/// before the end of the test.
73-
FakeImageHttpClient prepareBoringImageHttpClient() {
74-
final httpClient = FakeImageHttpClient();
75-
debugNetworkImageHttpClientProvider = () => httpClient;
76-
httpClient.request.response
77-
..statusCode = HttpStatus.ok
78-
..content = kSolidBlueAvatar;
79-
return httpClient;
80-
}
81-
8283
group('Heading', () {
8384
testWidgets('plain h6', (tester) async {
8485
await prepareContentBare(tester,

0 commit comments

Comments
 (0)