Skip to content

Commit 3f4d272

Browse files
committed
autocomplete: Implement new design for @-mention autocomplete items
Implemented new design for @-mention autocomplete items. Added new `contextMenuItemLabel` and `contextMenuItemMeta` color variables to `designVariables` class. Updated autocomplete tests. Relaxed avatar finder requirements so it accepts avatar URLs with size identifiers too. Fixes: #913
1 parent 061821c commit 3f4d272

File tree

3 files changed

+86
-26
lines changed

3 files changed

+86
-26
lines changed

lib/widgets/autocomplete.dart

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import 'package:flutter/material.dart';
22

3-
import 'content.dart';
4-
import 'store.dart';
3+
import '../api/model/model.dart';
54
import '../model/autocomplete.dart';
65
import '../model/compose.dart';
76
import '../model/narrow.dart';
87
import 'compose_box.dart';
8+
import 'content.dart';
9+
import 'store.dart';
10+
import 'text.dart';
11+
import 'theme.dart';
912

1013
abstract class AutocompleteField<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult> extends StatefulWidget {
1114
const AutocompleteField({
@@ -193,27 +196,61 @@ class ComposeAutocomplete extends AutocompleteField<MentionAutocompleteQuery, Me
193196
);
194197
}
195198

199+
196200
@override
197201
Widget buildItem(BuildContext context, int index, MentionAutocompleteResult option) {
202+
final designVariables = DesignVariables.of(context);
198203
Widget avatar;
199204
String label;
205+
String? metadata;
206+
200207
switch (option) {
201208
case UserMentionAutocompleteResult(:var userId):
202-
avatar = Avatar(userId: userId, size: 32, borderRadius: 3);
203-
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
209+
final store = PerAccountStoreWidget.of(context);
210+
final user = store.users[userId]!;
211+
avatar = Avatar(userId: userId, size: 36, borderRadius: 4);
212+
label = user.fullName;
213+
metadata = getDisplayEmailFor(user, store: store);
204214
}
205215
return InkWell(
206216
onTap: () {
207217
_onTapOption(context, option);
208218
},
219+
highlightColor: designVariables.editorButtonPressedBg,
220+
splashFactory: NoSplash.splashFactory,
221+
splashColor: Colors.transparent,
222+
borderRadius: BorderRadius.circular(8),
209223
child: Padding(
210-
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
224+
padding: const EdgeInsets.only(left: 4, right: 8, top: 4, bottom: 4),
211225
child: Row(
212226
children: [
213227
avatar,
214-
const SizedBox(width: 8),
215-
Text(label),
216-
])));
228+
const SizedBox(width: 6),
229+
Expanded(
230+
child: Column(
231+
mainAxisSize: MainAxisSize.min,
232+
crossAxisAlignment: CrossAxisAlignment.start,
233+
children: [
234+
Text(
235+
style: TextStyle(
236+
fontSize: 18,
237+
height: 20/18,
238+
color: designVariables.contextMenuItemLabel)
239+
.merge(weightVariableTextStyle(context, wght: 600)),
240+
overflow: TextOverflow.ellipsis,
241+
maxLines: 1,
242+
label,
243+
244+
),
245+
if(metadata != null) Text(
246+
style: TextStyle(
247+
fontSize: 14,
248+
height: 16/14,
249+
color: designVariables.contextMenuItemMeta),
250+
overflow: TextOverflow.ellipsis,
251+
maxLines: 1,
252+
metadata,
253+
)]))])));
217254
}
218255
}
219256

lib/widgets/theme.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
110110
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15),
111111
bgTopBar: const Color(0xfff5f5f5),
112112
borderBar: const Color(0x33000000),
113+
contextMenuItemLabel: const Color(0xff242631),
114+
contextMenuItemMeta: const Color(0xff626573),
115+
editorButtonPressedBg: const Color(0xff000000).withValues(alpha: 0.06),
113116
icon: const Color(0xff666699),
114117
labelCounterUnread: const Color(0xff222222),
115118
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(),
@@ -139,6 +142,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
139142
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
140143
bgTopBar: const Color(0xff242424),
141144
borderBar: Colors.black.withValues(alpha: 0.41),
145+
contextMenuItemLabel: const Color(0xffdfe1e8),
146+
contextMenuItemMeta: const Color(0xff9194a3),
147+
editorButtonPressedBg: const Color(0xffffffff).withValues(alpha: 0.06),
142148
icon: const Color(0xff7070c2),
143149
labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7),
144150
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(),
@@ -174,6 +180,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
174180
required this.bgCounterUnread,
175181
required this.bgTopBar,
176182
required this.borderBar,
183+
required this.contextMenuItemLabel,
184+
required this.contextMenuItemMeta,
185+
required this.editorButtonPressedBg,
177186
required this.icon,
178187
required this.labelCounterUnread,
179188
required this.labelEdited,
@@ -211,6 +220,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
211220
final Color bgCounterUnread;
212221
final Color bgTopBar;
213222
final Color borderBar;
223+
final Color contextMenuItemLabel;
224+
final Color contextMenuItemMeta;
225+
final Color editorButtonPressedBg;
214226
final Color icon;
215227
final Color labelCounterUnread;
216228
final Color labelEdited;
@@ -243,6 +255,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
243255
Color? bgCounterUnread,
244256
Color? bgTopBar,
245257
Color? borderBar,
258+
Color? contextMenuItemLabel,
259+
Color? contextMenuItemMeta,
260+
Color? editorButtonPressedBg,
246261
Color? icon,
247262
Color? labelCounterUnread,
248263
Color? labelEdited,
@@ -270,6 +285,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
270285
bgCounterUnread: bgCounterUnread ?? this.bgCounterUnread,
271286
bgTopBar: bgTopBar ?? this.bgTopBar,
272287
borderBar: borderBar ?? this.borderBar,
288+
contextMenuItemLabel: contextMenuItemLabel ?? this.contextMenuItemLabel,
289+
contextMenuItemMeta: contextMenuItemMeta ?? this.contextMenuItemMeta,
290+
editorButtonPressedBg: editorButtonPressedBg ?? this.editorButtonPressedBg,
273291
icon: icon ?? this.icon,
274292
labelCounterUnread: labelCounterUnread ?? this.labelCounterUnread,
275293
labelEdited: labelEdited ?? this.labelEdited,
@@ -304,6 +322,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
304322
bgCounterUnread: Color.lerp(bgCounterUnread, other.bgCounterUnread, t)!,
305323
bgTopBar: Color.lerp(bgTopBar, other.bgTopBar, t)!,
306324
borderBar: Color.lerp(borderBar, other.borderBar, t)!,
325+
contextMenuItemMeta: Color.lerp(contextMenuItemMeta, other.contextMenuItemMeta, t)!,
326+
contextMenuItemLabel: Color.lerp(contextMenuItemLabel, other.contextMenuItemLabel, t)!,
327+
editorButtonPressedBg: Color.lerp(editorButtonPressedBg, other.editorButtonPressedBg, t)!,
307328
icon: Color.lerp(icon, other.icon, t)!,
308329
labelCounterUnread: Color.lerp(labelCounterUnread, other.labelCounterUnread, t)!,
309330
labelEdited: Color.lerp(labelEdited, other.labelEdited, t)!,

test/widgets/autocomplete_test.dart

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,37 +108,39 @@ void main() {
108108
group('ComposeAutocomplete', () {
109109

110110
Finder findNetworkImage(String url) {
111+
String trimmedUrl = url.substring(0, url.length -4); //Remove the extension (.png) from the URL
111112
return find.byWidgetPredicate((widget) => switch(widget) {
112-
Image(image: NetworkImage(url: var imageUrl)) when imageUrl == url
113-
=> true,
113+
Image(image: NetworkImage(url: var imageUrl)) when imageUrl.startsWith(trimmedUrl)
114+
=> true,
114115
_ => false,
115116
});
116117
}
117118

118119
void checkUserShown(User user, PerAccountStore store, {required bool expected}) {
119120
check(find.text(user.fullName).evaluate().length).equals(expected ? 1 : 0);
121+
check(find.text(user.deliveryEmail!).evaluate().length).equals(expected ? 1 : 0);
120122
final avatarFinder =
121123
findNetworkImage(store.tryResolveUrl(user.avatarUrl!).toString());
122124
check(avatarFinder.evaluate().length).equals(expected ? 1 : 0);
123125
}
124126

125127
testWidgets('options appear, disappear, and change correctly', (tester) async {
126-
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png');
127-
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png');
128-
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png');
129-
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
130-
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
131-
132-
// Options are filtered correctly for query
133-
// TODO(#226): Remove this extra edit when this bug is fixed.
134-
await tester.enterText(composeInputFinder, 'hello @user ');
135-
await tester.enterText(composeInputFinder, 'hello @user t');
136-
await tester.pumpAndSettle(); // async computation; options appear
137-
138-
// "User Two" and "User Three" appear, but not "User One"
139-
checkUserShown(user1, store, expected: false);
140-
checkUserShown(user2, store, expected: true);
141-
checkUserShown(user3, store, expected: true);
128+
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png', deliveryEmail: '[email protected]');
129+
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png', deliveryEmail: '[email protected]');
130+
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png', deliveryEmail: '[email protected]');
131+
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
132+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
133+
134+
// Options are filtered correctly for query
135+
// TODO(#226): Remove this extra edit when this bug is fixed.
136+
await tester.enterText(composeInputFinder, 'hello @user ');
137+
await tester.enterText(composeInputFinder, 'hello @user t');
138+
await tester.pumpAndSettle(); // async computation; options appear
139+
140+
// "User Two" and "User Three" appear, but not "User One"
141+
checkUserShown(user1, store, expected: false);
142+
checkUserShown(user2, store, expected: true);
143+
checkUserShown(user3, store, expected: true);
142144

143145
// Finishing autocomplete updates compose box; causes options to disappear
144146
await tester.tap(find.text('User Three'));

0 commit comments

Comments
 (0)