Skip to content

Commit 61b64cd

Browse files
fombalangPIG208
andcommitted
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. Fixes: #913 Co-authored-by: Zixuan James Li <[email protected]> Signed-off-by: Zixuan James Li <[email protected]>
1 parent 1aca726 commit 61b64cd

File tree

3 files changed

+126
-21
lines changed

3 files changed

+126
-21
lines changed

lib/widgets/autocomplete.dart

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import '../model/autocomplete.dart';
1111
import '../model/compose.dart';
1212
import '../model/narrow.dart';
1313
import 'compose_box.dart';
14+
import 'text.dart';
15+
import 'theme.dart';
1416

1517
abstract class AutocompleteField<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult> extends StatefulWidget {
1618
const AutocompleteField({
@@ -218,6 +220,8 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
218220

219221
@override
220222
Widget buildItem(BuildContext context, int index, ComposeAutocompleteResult option) {
223+
final designVariables = DesignVariables.of(context);
224+
221225
final child = switch (option) {
222226
MentionAutocompleteResult() => _MentionAutocompleteItem(
223227
option: option, narrow: narrow),
@@ -227,6 +231,9 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
227231
onTap: () {
228232
_onTapOption(context, option);
229233
},
234+
highlightColor: designVariables.editorButtonPressedBg,
235+
splashFactory: NoSplash.splashFactory,
236+
borderRadius: BorderRadius.circular(5),
230237
child: child);
231238
}
232239
}
@@ -237,14 +244,14 @@ class _MentionAutocompleteItem extends StatelessWidget {
237244
final MentionAutocompleteResult option;
238245
final Narrow narrow;
239246

240-
Widget wildcardLabel(WildcardMentionOption wildcardOption, {
247+
String wildcardSublabel(WildcardMentionOption wildcardOption, {
241248
required BuildContext context,
242249
required PerAccountStore store,
243250
}) {
244251
final isDmNarrow = narrow is DmNarrow;
245252
final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9)
246253
final localizations = ZulipLocalizations.of(context);
247-
final description = switch (wildcardOption) {
254+
return switch (wildcardOption) {
248255
WildcardMentionOption.all || WildcardMentionOption.everyone => isDmNarrow
249256
? localizations.wildcardMentionAllDmDescription
250257
: isChannelWildcardAvailable
@@ -256,32 +263,61 @@ class _MentionAutocompleteItem extends StatelessWidget {
256263
: localizations.wildcardMentionStreamDescription,
257264
WildcardMentionOption.topic => localizations.wildcardMentionTopicDescription,
258265
};
259-
return Text.rich(TextSpan(text: '${wildcardOption.canonicalString} ', children: [
260-
TextSpan(text: description, style: TextStyle(fontSize: 12,
261-
color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.8)))]));
262266
}
263267

264268
@override
265269
Widget build(BuildContext context) {
266270
final store = PerAccountStoreWidget.of(context);
271+
final designVariables = DesignVariables.of(context);
272+
267273
Widget avatar;
268-
Widget label;
274+
String label;
275+
String? sublabel;
269276
switch (option) {
270277
case UserMentionAutocompleteResult(:var userId):
271278
final user = store.getUser(userId)!; // must exist because UserMentionAutocompleteResult
272-
avatar = Avatar(userId: userId, size: 32, borderRadius: 3); // web uses 21px
273-
label = Text(user.fullName);
279+
avatar = Avatar(userId: userId, size: 36, borderRadius: 4);
280+
label = user.fullName;
281+
sublabel = store.userDisplayEmail(user);
274282
case WildcardMentionAutocompleteResult(:var wildcardOption):
275-
avatar = const Icon(ZulipIcons.three_person, size: 29); // web uses 19px
276-
label = wildcardLabel(wildcardOption, context: context, store: store);
283+
avatar = SizedBox.square(dimension: 36,
284+
child: const Icon(ZulipIcons.three_person, size: 24));
285+
label = wildcardOption.canonicalString;
286+
sublabel = wildcardSublabel(wildcardOption, context: context, store: store);
277287
}
278288

289+
final labelWidget = Text(
290+
label,
291+
style: TextStyle(
292+
fontSize: 18,
293+
height: 20 / 18,
294+
color: designVariables.contextMenuItemLabel,
295+
).merge(weightVariableTextStyle(context,
296+
wght: sublabel == null ? 500 : 600)),
297+
overflow: TextOverflow.ellipsis,
298+
maxLines: 1);
299+
300+
final sublabelWidget = sublabel == null ? null : Text(
301+
sublabel,
302+
style: TextStyle(
303+
fontSize: 14,
304+
height: 16 / 14,
305+
color: designVariables.contextMenuItemMeta),
306+
overflow: TextOverflow.ellipsis,
307+
maxLines: 1);
308+
279309
return Padding(
280-
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
310+
padding: const EdgeInsetsDirectional.fromSTEB(4, 4, 8, 4),
281311
child: Row(children: [
282312
avatar,
283-
const SizedBox(width: 8),
284-
label,
313+
const SizedBox(width: 6),
314+
Expanded(child: Column(
315+
mainAxisSize: MainAxisSize.min,
316+
crossAxisAlignment: CrossAxisAlignment.start,
317+
children: [
318+
labelWidget,
319+
if (sublabelWidget != null) sublabelWidget,
320+
])),
285321
]));
286322
}
287323
}

lib/widgets/theme.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
136136
composeBoxBg: const Color(0xffffffff),
137137
contextMenuCancelText: const Color(0xff222222),
138138
contextMenuItemBg: const Color(0xff6159e1),
139+
contextMenuItemLabel: const Color(0xff242631),
140+
contextMenuItemMeta: const Color(0xff626573),
139141
contextMenuItemText: const Color(0xff381da7),
140142
editorButtonPressedBg: Colors.black.withValues(alpha: 0.06),
141143
foreground: const Color(0xff000000),
@@ -184,6 +186,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
184186
composeBoxBg: const Color(0xff0f0f0f),
185187
contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75),
186188
contextMenuItemBg: const Color(0xff7977fe),
189+
contextMenuItemLabel: const Color(0xffdfe1e8),
190+
contextMenuItemMeta: const Color(0xff9194a3),
187191
contextMenuItemText: const Color(0xff9398fd),
188192
editorButtonPressedBg: Colors.white.withValues(alpha: 0.06),
189193
foreground: const Color(0xffffffff),
@@ -240,6 +244,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
240244
required this.composeBoxBg,
241245
required this.contextMenuCancelText,
242246
required this.contextMenuItemBg,
247+
required this.contextMenuItemLabel,
248+
required this.contextMenuItemMeta,
243249
required this.contextMenuItemText,
244250
required this.editorButtonPressedBg,
245251
required this.foreground,
@@ -297,6 +303,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
297303
final Color composeBoxBg;
298304
final Color contextMenuCancelText;
299305
final Color contextMenuItemBg;
306+
final Color contextMenuItemLabel;
307+
final Color contextMenuItemMeta;
300308
final Color contextMenuItemText;
301309
final Color editorButtonPressedBg;
302310
final Color foreground;
@@ -349,6 +357,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
349357
Color? composeBoxBg,
350358
Color? contextMenuCancelText,
351359
Color? contextMenuItemBg,
360+
Color? contextMenuItemLabel,
361+
Color? contextMenuItemMeta,
352362
Color? contextMenuItemText,
353363
Color? editorButtonPressedBg,
354364
Color? foreground,
@@ -396,6 +406,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
396406
composeBoxBg: composeBoxBg ?? this.composeBoxBg,
397407
contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText,
398408
contextMenuItemBg: contextMenuItemBg ?? this.contextMenuItemBg,
409+
contextMenuItemLabel: contextMenuItemLabel ?? this.contextMenuItemLabel,
410+
contextMenuItemMeta: contextMenuItemMeta ?? this.contextMenuItemMeta,
399411
contextMenuItemText: contextMenuItemText ?? this.contextMenuItemBg,
400412
editorButtonPressedBg: editorButtonPressedBg ?? this.editorButtonPressedBg,
401413
foreground: foreground ?? this.foreground,
@@ -450,6 +462,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
450462
composeBoxBg: Color.lerp(composeBoxBg, other.composeBoxBg, t)!,
451463
contextMenuCancelText: Color.lerp(contextMenuCancelText, other.contextMenuCancelText, t)!,
452464
contextMenuItemBg: Color.lerp(contextMenuItemBg, other.contextMenuItemBg, t)!,
465+
contextMenuItemLabel: Color.lerp(contextMenuItemLabel, other.contextMenuItemLabel, t)!,
466+
contextMenuItemMeta: Color.lerp(contextMenuItemMeta, other.contextMenuItemMeta, t)!,
453467
contextMenuItemText: Color.lerp(contextMenuItemText, other.contextMenuItemBg, t)!,
454468
editorButtonPressedBg: Color.lerp(editorButtonPressedBg, other.editorButtonPressedBg, t)!,
455469
foreground: Color.lerp(foreground, other.foreground, t)!,

test/widgets/autocomplete_test.dart

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import 'package:zulip/model/narrow.dart';
1414
import 'package:zulip/model/store.dart';
1515
import 'package:zulip/model/typing_status.dart';
1616
import 'package:zulip/widgets/compose_box.dart';
17-
import 'package:zulip/widgets/icons.dart';
1817
import 'package:zulip/widgets/content.dart';
1918
import 'package:zulip/widgets/message_list.dart';
2019

@@ -201,11 +200,7 @@ void main() {
201200
});
202201

203202
void checkWildcardShown(WildcardMentionOption wildcard, {required bool expected}) {
204-
final richTextFinder = find.textContaining(wildcard.canonicalString, findRichText: true);
205-
final iconFinder = find.byIcon(ZulipIcons.three_person);
206-
final wildcardItemFinder = find.ancestor(of: richTextFinder,
207-
matching: find.ancestor(of: iconFinder, matching: find.byType(Row)));
208-
check(wildcardItemFinder).findsExactly(expected ? 1 : 0);
203+
check(find.text(wildcard.canonicalString)).findsExactly(expected ? 1 : 0);
209204
}
210205

211206
testWidgets('wildcard options appear, disappear, and change correctly', (tester) async {
@@ -226,8 +221,7 @@ void main() {
226221
checkWildcardShown(WildcardMentionOption.stream, expected: false);
227222

228223
// Finishing autocomplete updates compose box; causes options to disappear
229-
await tester.tap(find.textContaining(WildcardMentionOption.channel.canonicalString,
230-
findRichText: true));
224+
await tester.tap(find.text(WildcardMentionOption.channel.canonicalString));
231225
await tester.pump();
232226
check(tester.widget<TextField>(composeInputFinder).controller!.text)
233227
.contains(wildcardMention(WildcardMentionOption.channel, store: store));
@@ -239,6 +233,67 @@ void main() {
239233

240234
debugNetworkImageHttpClientProvider = null;
241235
});
236+
237+
group('sublabel', () {
238+
Finder findLabelsForItem({required Finder itemFinder}) {
239+
final itemColumn = find.ancestor(
240+
of: itemFinder,
241+
matching: find.byType(Column),
242+
).first;
243+
return find.descendant(of: itemColumn, matching: find.byType(Text));
244+
}
245+
246+
testWidgets('no sublabel when delivery email is unavailable', (tester) async {
247+
final user = eg.user(fullName: 'User One', deliveryEmail: null);
248+
final composeInputFinder = await setupToComposeInput(tester, users: [user]);
249+
250+
// TODO(#226): Remove this extra edit when this bug is fixed.
251+
await tester.enterText(composeInputFinder, 'hello @user ');
252+
await tester.enterText(composeInputFinder, 'hello @user o');
253+
await tester.pumpAndSettle(); // async computation; options appear
254+
255+
checkUserShown(user, expected: true);
256+
check(find.text(user.email)).findsNothing();
257+
check(findLabelsForItem(
258+
itemFinder: find.text(user.fullName))).findsOne();
259+
260+
debugNetworkImageHttpClientProvider = null;
261+
});
262+
263+
testWidgets('show sublabel when delivery email is available', (tester) async {
264+
final user = eg.user(fullName: 'User One', deliveryEmail: '[email protected]');
265+
final composeInputFinder = await setupToComposeInput(tester, users: [user]);
266+
267+
// TODO(#226): Remove this extra edit when this bug is fixed.
268+
await tester.enterText(composeInputFinder, 'hello @user ');
269+
await tester.enterText(composeInputFinder, 'hello @user o');
270+
await tester.pumpAndSettle(); // async computation; options appear
271+
272+
checkUserShown(user, expected: true);
273+
check(find.text(user.deliveryEmail!)).findsOne();
274+
check(findLabelsForItem(
275+
itemFinder: find.text(user.fullName))).findsExactly(2);
276+
277+
debugNetworkImageHttpClientProvider = null;
278+
});
279+
280+
testWidgets('show sublabel for wildcard mention items', (tester) async {
281+
final composeInputFinder = await setupToComposeInput(tester,
282+
narrow: const ChannelNarrow(1));
283+
284+
// TODO(#226): Remove this extra edit when this bug is fixed.
285+
await tester.enterText(composeInputFinder, '@chann');
286+
await tester.enterText(composeInputFinder, '@channe');
287+
await tester.pumpAndSettle(); // async computation; options appear
288+
289+
checkWildcardShown(WildcardMentionOption.channel, expected: true);
290+
check(find.text('Notify channel')).findsOne();
291+
check(findLabelsForItem(
292+
itemFinder: find.text('channel'))).findsExactly(2);
293+
294+
debugNetworkImageHttpClientProvider = null;
295+
});
296+
});
242297
});
243298

244299
group('emoji', () {

0 commit comments

Comments
 (0)